Skip to content

Commit af49549

Browse files
committed
Support pre_comment and post_comment options in ecto.
1 parent 1544cf4 commit af49549

10 files changed

Lines changed: 139 additions & 94 deletions

File tree

integration_test/sql/logging.exs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,46 +178,58 @@ defmodule Ecto.Integration.LoggingTest do
178178
end
179179
end
180180

181-
describe ":label option" do
182-
test "prepends a leading comment to query operations" do
181+
describe ":comments option" do
182+
test "comments query operations" do
183183
assert capture_log(fn ->
184-
TestRepo.all(Post, label: "list_posts_q", log: :error)
184+
TestRepo.all(Post, comments: [pre: "list_posts_q"], log: :error)
185185
end) =~ "/* list_posts_q */ SELECT"
186186

187187
assert capture_log(fn ->
188-
TestRepo.update_all(Post, [set: [visits: 0]], label: "reset_visits_q", log: :error)
188+
TestRepo.update_all(Post, [set: [visits: 0]], comments: [pre: "reset_visits_q"], log: :error)
189189
end) =~ "/* reset_visits_q */ UPDATE"
190190

191191
assert capture_log(fn ->
192-
TestRepo.delete_all(Post, label: "purge_posts_q", log: :error)
192+
TestRepo.delete_all(Post, comments: [pre: "purge_posts_q"], log: :error)
193193
end) =~ "/* purge_posts_q */ DELETE"
194194
end
195195

196-
test "prepends a leading comment to insert/update/delete/insert_all" do
196+
test "supports both :pre and :post" do
197197
assert capture_log(fn ->
198-
TestRepo.insert!(%Post{title: "1"}, label: "insert_create_post_q", log: :error)
198+
TestRepo.all(Post, comments: [pre: "before_q", post: "after_q"], log: :error)
199+
end) =~ ~r{/\* before_q \*/ SELECT.* /\* after_q \*/}
200+
end
201+
202+
test "renders with query_cache: false (the escape hatch for dynamic comments)" do
203+
assert capture_log(fn ->
204+
TestRepo.all(Post, comments: [pre: "dyn_#{System.unique_integer()}"], query_cache: false, log: :error)
205+
end) =~ ~r{/\* dyn_-?\d+ \*/ SELECT}
206+
end
207+
208+
test "comments insert/update/delete/insert_all" do
209+
assert capture_log(fn ->
210+
TestRepo.insert!(%Post{title: "1"}, comments: [pre: "insert_create_post_q"], log: :error)
199211
end) =~ "/* insert_create_post_q */ INSERT INTO"
200212

201213
post = TestRepo.insert!(%Post{title: "x"})
202214

203215
assert capture_log(fn ->
204216
post
205217
|> Ecto.Changeset.change(title: "y")
206-
|> TestRepo.update!(label: "update_post_q", log: :error)
218+
|> TestRepo.update!(comments: [pre: "update_post_q"], log: :error)
207219
end) =~ "/* update_post_q */ UPDATE"
208220

209221
assert capture_log(fn ->
210-
TestRepo.delete!(post, label: "delete_post_q", log: :error)
222+
TestRepo.delete!(post, comments: [pre: "delete_post_q"], log: :error)
211223
end) =~ "/* delete_post_q */ DELETE"
212224

213225
assert capture_log(fn ->
214-
TestRepo.insert_all(Post, [%{title: "a"}], label: "bulk_insert_posts_q", log: :error)
226+
TestRepo.insert_all(Post, [%{title: "a"}], comments: [pre: "bulk_insert_posts_q"], log: :error)
215227
end) =~ "/* bulk_insert_posts_q */ INSERT INTO"
216228
end
217229

218-
test "rejects a label that could break out of the comment block" do
230+
test "rejects a comment that could break out of the comment block" do
219231
assert_raise ArgumentError, ~r/cannot contain/, fn ->
220-
TestRepo.insert!(%Post{title: "1"}, label: "evil */ DROP TABLE posts")
232+
TestRepo.insert!(%Post{title: "1"}, comments: [pre: "evil */ DROP TABLE posts"])
221233
end
222234
end
223235
end

lib/ecto/adapters/myxql.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,8 @@ defmodule Ecto.Adapters.MyXQL do
381381
end
382382

383383
# This adapter overrides insert/6 instead of going through
384-
# Ecto.Adapters.SQL.struct/10, so prepend the `:label` comment here too.
385-
{sql, opts} = Ecto.Adapters.SQL.prepend_label(sql, opts)
384+
# Ecto.Adapters.SQL.struct/10, so wrap the `:comments` here too.
385+
sql = Ecto.Adapters.SQL.wrap_comments(sql, opts)
386386

387387
case Ecto.Adapters.SQL.query(adapter_meta, sql, values ++ query_params, opts) do
388388
{:ok, %{num_rows: 0}} ->

lib/ecto/adapters/myxql/connection.ex

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ if Code.ensure_loaded?(MyXQL) do
118118
limit = limit(query, sources)
119119
offset = offset(query, sources)
120120
lock = lock(query, sources)
121-
label = label(query)
121+
{pre_comments, post_comments} = SQL.comments(query.comments)
122122

123123
[
124-
label,
124+
pre_comments,
125125
cte,
126126
select,
127127
from,
@@ -133,7 +133,8 @@ if Code.ensure_loaded?(MyXQL) do
133133
combinations,
134134
order_by,
135135
limit,
136-
offset | lock
136+
offset,
137+
lock | post_comments
137138
]
138139
end
139140

@@ -147,7 +148,7 @@ if Code.ensure_loaded?(MyXQL) do
147148

148149
sources = create_names(query, [])
149150
cte = cte(query, sources)
150-
label = label(query)
151+
{pre_comments, post_comments} = SQL.comments(query.comments)
151152
{from, name} = get_source(query, sources, 0, source)
152153

153154
fields =
@@ -161,7 +162,7 @@ if Code.ensure_loaded?(MyXQL) do
161162
prefix = prefix || ["UPDATE ", from, " AS ", name, join, " SET "]
162163
where = where(%{query | wheres: wheres ++ query.wheres}, sources)
163164

164-
[label, cte, prefix, fields | where]
165+
[pre_comments, cte, prefix, fields, where | post_comments]
165166
end
166167

167168
@impl true
@@ -174,12 +175,12 @@ if Code.ensure_loaded?(MyXQL) do
174175
cte = cte(query, sources)
175176
{_, name, _} = elem(sources, 0)
176177

177-
label = label(query)
178+
{pre_comments, post_comments} = SQL.comments(query.comments)
178179
from = from(query, sources)
179180
join = join(query, sources)
180181
where = where(query, sources)
181182

182-
[label, cte, "DELETE ", name, ".*", from, join | where]
183+
[pre_comments, cte, "DELETE ", name, ".*", from, join, where | post_comments]
183184
end
184185

185186
@impl true
@@ -640,9 +641,6 @@ if Code.ensure_loaded?(MyXQL) do
640641
end)
641642
end
642643

643-
defp label(%{label: nil}), do: []
644-
defp label(%{label: label}), do: ["/* ", label, " */ "]
645-
646644
defp lock(%{lock: nil}, _sources), do: []
647645
defp lock(%{lock: binary}, _sources) when is_binary(binary), do: [?\s | binary]
648646
defp lock(%{lock: expr} = query, sources), do: [?\s | expr(expr, sources, query)]

lib/ecto/adapters/postgres/connection.ex

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ if Code.ensure_loaded?(Postgrex) do
191191
limit = limit(query, sources)
192192
offset = offset(query, sources)
193193
lock = lock(query, sources)
194-
label = label(query)
194+
{pre_comments, post_comments} = Ecto.Adapters.SQL.comments(query.comments)
195195

196196
[
197-
label,
197+
pre_comments,
198198
cte,
199199
select,
200200
from,
@@ -206,7 +206,8 @@ if Code.ensure_loaded?(Postgrex) do
206206
combinations,
207207
order_by,
208208
limit,
209-
offset | lock
209+
offset,
210+
lock | post_comments
210211
]
211212
end
212213

@@ -215,25 +216,25 @@ if Code.ensure_loaded?(Postgrex) do
215216
sources = create_names(query, [])
216217
cte = cte(query, sources)
217218
{from, name} = get_source(query, sources, 0, source)
218-
label = label(query)
219+
{pre_comments, post_comments} = Ecto.Adapters.SQL.comments(query.comments)
219220
prefix = prefix || ["UPDATE ", from, " AS ", name | " SET "]
220221
fields = update_fields(query, sources)
221222
{join, wheres} = using_join(query, :update_all, "FROM", sources)
222223
where = where(%{query | wheres: wheres ++ query.wheres}, sources)
223224

224-
[label, cte, prefix, fields, join, where | returning(query, sources)]
225+
[pre_comments, cte, prefix, fields, join, where, returning(query, sources) | post_comments]
225226
end
226227

227228
@impl true
228229
def delete_all(%{from: from} = query) do
229230
sources = create_names(query, [])
230231
cte = cte(query, sources)
231232
{from, name} = get_source(query, sources, 0, from)
232-
label = label(query)
233+
{pre_comments, post_comments} = Ecto.Adapters.SQL.comments(query.comments)
233234
{join, wheres} = using_join(query, :delete_all, "USING", sources)
234235
where = where(%{query | wheres: wheres ++ query.wheres}, sources)
235236

236-
[label, cte, "DELETE FROM ", from, " AS ", name, join, where | returning(query, sources)]
237+
[pre_comments, cte, "DELETE FROM ", from, " AS ", name, join, where, returning(query, sources) | post_comments]
237238
end
238239

239240
@impl true
@@ -882,9 +883,6 @@ if Code.ensure_loaded?(Postgrex) do
882883
end)
883884
end
884885

885-
defp label(%{label: nil}), do: []
886-
defp label(%{label: label}), do: ["/* ", label, " */ "]
887-
888886
defp lock(%{lock: nil}, _sources), do: []
889887
defp lock(%{lock: binary}, _sources) when is_binary(binary), do: [?\s | binary]
890888
defp lock(%{lock: expr} = query, sources), do: [?\s | expr(expr, sources, query)]

lib/ecto/adapters/sql.ex

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ defmodule Ecto.Adapters.SQL do
987987
opts
988988
end
989989

990-
{sql, opts} = prepend_label(sql, opts)
990+
sql = wrap_comments(sql, opts)
991991

992992
all_params = placeholders ++ Enum.reverse(params, conflict_params)
993993

@@ -1169,28 +1169,39 @@ defmodule Ecto.Adapters.SQL do
11691169
end
11701170

11711171
@doc false
1172-
def prepend_label(sql, opts) do
1173-
case Keyword.get(opts, :label) do
1174-
nil ->
1175-
{sql, opts}
1176-
1177-
label ->
1178-
label = validate_label!(label)
1179-
{["/* ", label, " */ ", sql], opts}
1180-
end
1172+
def wrap_comments(sql, opts) do
1173+
{pre, post} = comments(Keyword.get(opts, :comments, []))
1174+
[pre, sql | post]
1175+
end
1176+
1177+
@doc false
1178+
def comments(comments) when is_list(comments) do
1179+
{pre, post} =
1180+
Enum.reduce(comments, {[], []}, fn
1181+
{:pre, c}, {pre, post} -> {[["/* ", escape_comment!(c), " */ "] | pre], post}
1182+
{:post, c}, {pre, post} -> {pre, [[" /* ", escape_comment!(c), " */"] | post]}
1183+
other, _ -> raise ArgumentError, "expected {:pre, string} or {:post, string}, got: #{inspect(other)}"
1184+
end)
1185+
1186+
{Enum.reverse(pre), Enum.reverse(post)}
1187+
end
1188+
1189+
def comments(other) do
1190+
raise ArgumentError,
1191+
"comments must be a keyword list of [pre: string, post: string], got: #{inspect(other)}"
11811192
end
11821193

1183-
defp validate_label!(label) when is_binary(label) do
1184-
if String.contains?(label, ["/*", "*/", <<0>>]) do
1194+
defp escape_comment!(comment) when is_binary(comment) do
1195+
if String.contains?(comment, ["/*", "*/", <<0>>]) do
11851196
raise ArgumentError,
1186-
"a label cannot contain `/*`, `*/`, or null bytes, got: #{inspect(label)}. "
1197+
"a comment cannot contain `/*`, `*/`, or null bytes, got: #{inspect(comment)}. "
11871198
end
11881199

1189-
label
1200+
comment
11901201
end
11911202

1192-
defp validate_label!(other) do
1193-
raise ArgumentError, "a label must be a string, got: #{inspect(other)}"
1203+
defp escape_comment!(other) do
1204+
raise ArgumentError, "a comment must be a string, got: #{inspect(other)}"
11941205
end
11951206

11961207
@doc false
@@ -1213,7 +1224,7 @@ defmodule Ecto.Adapters.SQL do
12131224
opts
12141225
end
12151226

1216-
{sql, opts} = prepend_label(sql, opts)
1227+
sql = wrap_comments(sql, opts)
12171228

12181229
case query(adapter_meta, sql, values, [source: source] ++ opts) do
12191230
{:ok, %{rows: nil, num_rows: 1}} ->

lib/ecto/adapters/tds/connection.ex

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ if Code.ensure_loaded?(Tds) do
170170
# limit = is handled in select (TOP X)
171171
offset = offset(query, sources)
172172
lock = lock(query, sources)
173-
label = label(query)
173+
{pre_comments, post_comments} = SQL.comments(query.comments)
174174

175175
if query.offset != nil and query.order_bys == [],
176176
do: error!(query, "ORDER BY is mandatory when OFFSET is set")
177177

178-
[label, cte, select, from, join, where, group_by, having, combinations, order_by, lock | offset]
178+
[pre_comments, cte, select, from, join, where, group_by, having, combinations, order_by, lock, offset | post_comments]
179179
end
180180

181181
@impl true
@@ -189,10 +189,10 @@ if Code.ensure_loaded?(Tds) do
189189
join = join(query, sources)
190190
where = where(query, sources)
191191
lock = lock(query, sources)
192-
label = label(query)
192+
{pre_comments, post_comments} = SQL.comments(query.comments)
193193

194194
[
195-
label,
195+
pre_comments,
196196
cte,
197197
"UPDATE ",
198198
name,
@@ -201,7 +201,8 @@ if Code.ensure_loaded?(Tds) do
201201
returning(query, 0, "INSERTED"),
202202
from,
203203
join,
204-
where | lock
204+
where,
205+
lock | post_comments
205206
]
206207
end
207208

@@ -216,9 +217,9 @@ if Code.ensure_loaded?(Tds) do
216217
join = join(query, sources)
217218
where = where(query, sources)
218219
lock = lock(query, sources)
219-
label = label(query)
220+
{pre_comments, post_comments} = SQL.comments(query.comments)
220221

221-
[label, cte, delete, returning(query, 0, "DELETED"), from, join, where | lock]
222+
[pre_comments, cte, delete, returning(query, 0, "DELETED"), from, join, where, lock | post_comments]
222223
end
223224

224225
@impl true
@@ -660,9 +661,6 @@ if Code.ensure_loaded?(Tds) do
660661
defp hints([_ | _] = hints), do: [" WITH (", Enum.intersperse(hints, ", "), ?)]
661662
defp hints([]), do: []
662663

663-
defp label(%{label: nil}), do: []
664-
defp label(%{label: label}), do: ["/* ", label, " */ "]
665-
666664
defp lock(%{lock: nil}, _sources), do: []
667665
defp lock(%{lock: binary}, _sources) when is_binary(binary), do: [" OPTION (", binary, ?)]
668666
defp lock(%{lock: expr} = query, sources), do: [" OPTION (", expr(expr, sources, query), ?)]

test/ecto/adapters/myxql_test.exs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -590,15 +590,19 @@ defmodule Ecto.Adapters.MyXQLTest do
590590
assert all(query) == ~s{SELECT TRUE FROM `schema` AS s0 UPDATE on s0}
591591
end
592592

593-
test "label" do
593+
test "comments" do
594594
query = Schema |> select([], true) |> plan()
595-
assert all(%{query | label: "myquery"}) == ~s{/* myquery */ SELECT TRUE FROM `schema` AS s0}
595+
assert all(%{query | comments: [pre: "q"]}) == ~s{/* q */ SELECT TRUE FROM `schema` AS s0}
596+
assert all(%{query | comments: [post: "q"]}) == ~s{SELECT TRUE FROM `schema` AS s0 /* q */}
597+
598+
assert all(%{query | comments: [pre: "a", post: "b"]}) ==
599+
~s{/* a */ SELECT TRUE FROM `schema` AS s0 /* b */}
596600

597601
query = Schema |> update([], set: [x: 0]) |> plan(:update_all)
598-
assert update_all(%{query | label: "upd_q"}) == ~s{/* upd_q */ UPDATE `schema` AS s0 SET s0.`x` = 0}
602+
assert update_all(%{query | comments: [pre: "upd_q"]}) == ~s{/* upd_q */ UPDATE `schema` AS s0 SET s0.`x` = 0}
599603

600604
query = Schema |> plan(:delete_all)
601-
assert delete_all(%{query | label: "del_q"}) == ~s{/* del_q */ DELETE s0.* FROM `schema` AS s0}
605+
assert delete_all(%{query | comments: [pre: "del_q"]}) == ~s{/* del_q */ DELETE s0.* FROM `schema` AS s0}
602606
end
603607

604608
test "string escape" do

test/ecto/adapters/postgres_test.exs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -783,15 +783,19 @@ defmodule Ecto.Adapters.PostgresTest do
783783
assert all(query) == ~s{SELECT TRUE FROM "schema" AS s0 UPDATE on s0}
784784
end
785785

786-
test "label" do
786+
test "comments" do
787787
query = Schema |> select([], true) |> plan()
788-
assert all(%{query | label: "myquery"}) == ~s{/* myquery */ SELECT TRUE FROM "schema" AS s0}
788+
assert all(%{query | comments: [pre: "q"]}) == ~s{/* q */ SELECT TRUE FROM "schema" AS s0}
789+
assert all(%{query | comments: [post: "q"]}) == ~s{SELECT TRUE FROM "schema" AS s0 /* q */}
790+
791+
assert all(%{query | comments: [pre: "a", post: "b"]}) ==
792+
~s{/* a */ SELECT TRUE FROM "schema" AS s0 /* b */}
789793

790794
query = Schema |> update([], set: [x: 0]) |> plan(:update_all)
791-
assert update_all(%{query | label: "upd_q"}) == ~s{/* upd_q */ UPDATE "schema" AS s0 SET "x" = 0}
795+
assert update_all(%{query | comments: [pre: "upd_q"]}) == ~s{/* upd_q */ UPDATE "schema" AS s0 SET "x" = 0}
792796

793797
query = Schema |> plan(:delete_all)
794-
assert delete_all(%{query | label: "del_q"}) == ~s{/* del_q */ DELETE FROM "schema" AS s0}
798+
assert delete_all(%{query | comments: [pre: "del_q"]}) == ~s{/* del_q */ DELETE FROM "schema" AS s0}
795799
end
796800

797801
test "string escape" do

0 commit comments

Comments
 (0)