Skip to content

Commit e5663b8

Browse files
author
João Paulo
authored
Fix CTE subqueries not finding parent bindings (#338)
1 parent b41a2a4 commit e5663b8

8 files changed

Lines changed: 190 additions & 37 deletions

File tree

lib/ecto/adapters/myxql/connection.ex

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,14 @@ if Code.ensure_loaded?(MyXQL) do
295295
[quote_name(name), " AS ", cte_query(cte, sources, query)]
296296
end
297297

298-
defp cte_query(%Ecto.Query{} = query, _, _), do: ["(", all(query), ")"]
299-
defp cte_query(%QueryExpr{expr: expr}, sources, query), do: expr(expr, sources, query)
298+
defp cte_query(%Ecto.Query{} = query, sources, parent_query) do
299+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
300+
["(", all(query, subquery_as_prefix(sources)), ")"]
301+
end
302+
303+
defp cte_query(%QueryExpr{expr: expr}, sources, query) do
304+
expr(expr, sources, query)
305+
end
300306

301307
defp update_fields(type, %{updates: updates} = query, sources) do
302308
fields = for(%{expr: expr} <- updates,
@@ -479,9 +485,10 @@ if Code.ensure_loaded?(MyXQL) do
479485
'?'
480486
end
481487

482-
defp expr({{:., _, [{:parent_as, _, [{:&, _, [idx]}]}, field]}, _, []}, _sources, query)
488+
defp expr({{:., _, [{:parent_as, _, [as]}, field]}, _, []}, _sources, query)
483489
when is_atom(field) do
484-
{_, name, _} = elem(query.aliases[@parent_as], idx)
490+
{ix, sources} = get_parent_sources_ix(query, as)
491+
{_, name, _} = elem(sources, ix)
485492
[name, ?. | quote_name(field)]
486493
end
487494

@@ -534,8 +541,8 @@ if Code.ensure_loaded?(MyXQL) do
534541
error!(query, "MySQL adapter does not support aggregate filters")
535542
end
536543

537-
defp expr(%Ecto.SubQuery{query: query}, sources, _query) do
538-
query = put_in(query.aliases[@parent_as], sources)
544+
defp expr(%Ecto.SubQuery{query: query}, sources, parent_query) do
545+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
539546
[?(, all(query, subquery_as_prefix(sources)), ?)]
540547
end
541548

@@ -993,6 +1000,13 @@ if Code.ensure_loaded?(MyXQL) do
9931000
{expr || expr(source, sources, query), name}
9941001
end
9951002

1003+
defp get_parent_sources_ix(query, as) do
1004+
case query.aliases[@parent_as] do
1005+
{%{aliases: %{^as => ix}}, sources} -> {ix, sources}
1006+
{%{} = parent, _sources} -> get_parent_sources_ix(parent, as)
1007+
end
1008+
end
1009+
9961010
defp quote_name(name) when is_atom(name),
9971011
do: quote_name(Atom.to_string(name))
9981012

lib/ecto/adapters/postgres/connection.ex

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,14 @@ if Code.ensure_loaded?(Postgrex) do
390390
[quote_name(name), " AS ", cte_query(cte, sources, query)]
391391
end
392392

393-
defp cte_query(%Ecto.Query{} = query, _, _), do: ["(", all(query), ")"]
394-
defp cte_query(%QueryExpr{expr: expr}, sources, query), do: expr(expr, sources, query)
393+
defp cte_query(%Ecto.Query{} = query, sources, parent_query) do
394+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
395+
["(", all(query, subquery_as_prefix(sources)), ")"]
396+
end
397+
398+
defp cte_query(%QueryExpr{expr: expr}, sources, query) do
399+
expr(expr, sources, query)
400+
end
395401

396402
defp update_fields(%{updates: updates} = query, sources) do
397403
for(%{expr: expr} <- updates,
@@ -585,9 +591,10 @@ if Code.ensure_loaded?(Postgrex) do
585591
[?$ | Integer.to_string(ix + 1)]
586592
end
587593

588-
defp expr({{:., _, [{:parent_as, _, [{:&, _, [idx]}]}, field]}, _, []}, _sources, query)
594+
defp expr({{:., _, [{:parent_as, _, [as]}, field]}, _, []}, _sources, query)
589595
when is_atom(field) do
590-
quote_qualified_name(field, query.aliases[@parent_as], idx)
596+
{ix, sources} = get_parent_sources_ix(query, as)
597+
quote_qualified_name(field, sources, ix)
591598
end
592599

593600
defp expr({{:., _, [{:&, _, [idx]}, field]}, _, []}, sources, _query) when is_atom(field) do
@@ -628,8 +635,8 @@ if Code.ensure_loaded?(Postgrex) do
628635
["NOT (", expr(expr, sources, query), ?)]
629636
end
630637

631-
defp expr(%Ecto.SubQuery{query: query}, sources, _query) do
632-
query = put_in(query.aliases[@parent_as], sources)
638+
defp expr(%Ecto.SubQuery{query: query}, sources, parent_query) do
639+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
633640
[?(, all(query, subquery_as_prefix(sources)), ?)]
634641
end
635642

@@ -1216,6 +1223,13 @@ if Code.ensure_loaded?(Postgrex) do
12161223
{expr || expr(source, sources, query), name}
12171224
end
12181225

1226+
defp get_parent_sources_ix(query, as) do
1227+
case query.aliases[@parent_as] do
1228+
{%{aliases: %{^as => ix}}, sources} -> {ix, sources}
1229+
{%{} = parent, _sources} -> get_parent_sources_ix(parent, as)
1230+
end
1231+
end
1232+
12191233
defp quote_qualified_name(name, sources, ix) do
12201234
{_, source, _} = elem(sources, ix)
12211235
[source, ?. | quote_name(name)]

lib/ecto/adapters/tds/connection.ex

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,10 @@ if Code.ensure_loaded?(Tds) do
457457
]
458458
end
459459

460-
defp cte_query(%Ecto.Query{} = query, _, _), do: [?(, all(query), ?)]
460+
defp cte_query(%Ecto.Query{} = query, sources, parent_query) do
461+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
462+
[?(, all(query, subquery_as_prefix(sources)), ?)]
463+
end
461464

462465
defp update_fields(%Query{updates: updates} = query, sources) do
463466
for(
@@ -661,9 +664,10 @@ if Code.ensure_loaded?(Tds) do
661664
"@#{idx + 1}"
662665
end
663666

664-
defp expr({{:., _, [{:parent_as, _, [{:&, _, [idx]}]}, field]}, _, []}, _sources, query)
667+
defp expr({{:., _, [{:parent_as, _, [as]}, field]}, _, []}, _sources, query)
665668
when is_atom(field) do
666-
{_, name, _} = elem(query.aliases[@parent_as], idx)
669+
{ix, sources} = get_parent_sources_ix(query, as)
670+
{_, name, _} = elem(sources, ix)
667671
[name, ?. | quote_name(field)]
668672
end
669673

@@ -734,8 +738,8 @@ if Code.ensure_loaded?(Tds) do
734738
error!(query, "Tds adapter does not support aggregate filters")
735739
end
736740

737-
defp expr(%Ecto.SubQuery{query: query}, sources, _query) do
738-
query = put_in(query.aliases[@parent_as], sources)
741+
defp expr(%Ecto.SubQuery{query: query}, sources, parent_query) do
742+
query = put_in(query.aliases[@parent_as], {parent_query, sources})
739743
[?(, all(query, subquery_as_prefix(sources)), ?)]
740744
end
741745

@@ -1482,6 +1486,13 @@ if Code.ensure_loaded?(Tds) do
14821486
{expr || expr(source, sources, query), name}
14831487
end
14841488

1489+
defp get_parent_sources_ix(query, as) do
1490+
case query.aliases[@parent_as] do
1491+
{%{aliases: %{^as => ix}}, sources} -> {ix, sources}
1492+
{%{} = parent, _sources} -> get_parent_sources_ix(parent, as)
1493+
end
1494+
end
1495+
14851496
defp quote_name(name) when is_atom(name) do
14861497
quote_name(Atom.to_string(name))
14871498
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ defmodule EctoSQL.MixProject do
7777
if path = System.get_env("ECTO_PATH") do
7878
{:ecto, path: path}
7979
else
80-
{:ecto, "~> 3.6.2"}
80+
{:ecto, github: "elixir-ecto/ecto"}
8181
end
8282
end
8383

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
77
"deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm", "e3bf435a54ed27b0ba3a01eb117ae017988804e136edcbe8a6a14c310daa966e"},
88
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
9-
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
9+
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "47ae9c8dcf8d6cdd5e67d18c0f56664b66d054f8", []},
1010
"ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"},
1111
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
1212
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},

test/ecto/adapters/myxql_test.exs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ defmodule Ecto.Adapters.MyXQLTest do
127127

128128
assert all(query) ==
129129
~s{WITH RECURSIVE `tree` AS } <>
130-
~s{(SELECT c0.`id` AS `id`, 1 AS `depth` FROM `categories` AS c0 WHERE (c0.`parent_id` IS NULL) } <>
130+
~s{(SELECT sc0.`id` AS `id`, 1 AS `depth` FROM `categories` AS sc0 WHERE (sc0.`parent_id` IS NULL) } <>
131131
~s{UNION ALL } <>
132132
~s{(SELECT c0.`id`, t1.`depth` + 1 FROM `categories` AS c0 } <>
133133
~s{INNER JOIN `tree` AS t1 ON t1.`id` = c0.`parent_id`)) } <>
@@ -166,8 +166,8 @@ defmodule Ecto.Adapters.MyXQLTest do
166166

167167
assert all(query) ==
168168
~s{WITH `comments_scope` AS (} <>
169-
~s{SELECT c0.`entity_id` AS `entity_id`, c0.`text` AS `text` } <>
170-
~s{FROM `comments` AS c0 WHERE (c0.`deleted_at` IS NULL)) } <>
169+
~s{SELECT sc0.`entity_id` AS `entity_id`, sc0.`text` AS `text` } <>
170+
~s{FROM `comments` AS sc0 WHERE (sc0.`deleted_at` IS NULL)) } <>
171171
~s{SELECT p0.`title`, c1.`text` } <>
172172
~s{FROM `posts` AS p0 } <>
173173
~s{INNER JOIN `comments_scope` AS c1 ON c1.`entity_id` = p0.`guid` } <>
@@ -206,7 +206,7 @@ defmodule Ecto.Adapters.MyXQLTest do
206206

207207
assert update_all(query) ==
208208
~s{WITH `target_rows` AS } <>
209-
~s{(SELECT s0.`id` AS `id` FROM `schema` AS s0 ORDER BY s0.`id` LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
209+
~s{(SELECT ss0.`id` AS `id` FROM `schema` AS ss0 ORDER BY ss0.`id` LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
210210
~s{UPDATE `schema` AS s0, `target_rows` AS t1 } <>
211211
~s{SET s0.`x` = 123 } <>
212212
~s{WHERE (t1.`id` = s0.`id`)}
@@ -224,12 +224,50 @@ defmodule Ecto.Adapters.MyXQLTest do
224224

225225
assert delete_all(query) ==
226226
~s{WITH `target_rows` AS } <>
227-
~s{(SELECT s0.`id` AS `id` FROM `schema` AS s0 ORDER BY s0.`id` LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
227+
~s{(SELECT ss0.`id` AS `id` FROM `schema` AS ss0 ORDER BY ss0.`id` LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
228228
~s{DELETE s0.* } <>
229229
~s{FROM `schema` AS s0 } <>
230230
~s{INNER JOIN `target_rows` AS t1 ON t1.`id` = s0.`id`}
231231
end
232232

233+
test "parent binding subquery and CTE" do
234+
initial_query =
235+
"categories"
236+
|> where([c], c.id == parent_as(:parent_category).id)
237+
|> select([:id, :parent_id])
238+
239+
iteration_query =
240+
"categories"
241+
|> join(:inner, [c], t in "tree", on: t.parent_id == c.id)
242+
|> select([:id, :parent_id])
243+
244+
cte_query = initial_query |> union_all(^iteration_query)
245+
246+
breadcrumbs_query =
247+
"tree"
248+
|> recursive_ctes(true)
249+
|> with_cte("tree", as: ^cte_query)
250+
|> select([t], %{breadcrumbs: fragment("GROUP_CONCAT(? SEPARATOR ' / ')", t.id)})
251+
252+
query =
253+
from(c in "categories",
254+
as: :parent_category,
255+
left_lateral_join: b in subquery(breadcrumbs_query),
256+
select: %{id: c.id, breadcrumbs: b.breadcrumbs}
257+
)
258+
|> plan()
259+
260+
assert all(query) ==
261+
~s{SELECT c0.`id`, s1.`breadcrumbs` FROM `categories` AS c0 } <>
262+
~s{LEFT OUTER JOIN LATERAL } <>
263+
~s{(WITH RECURSIVE `tree` AS } <>
264+
~s{(SELECT ssc0.`id` AS `id`, ssc0.`parent_id` AS `parent_id` FROM `categories` AS ssc0 WHERE (ssc0.`id` = c0.`id`) } <>
265+
~s{UNION ALL } <>
266+
~s{(SELECT c0.`id`, c0.`parent_id` FROM `categories` AS c0 } <>
267+
~s{INNER JOIN `tree` AS t1 ON t1.`parent_id` = c0.`id`)) } <>
268+
~s{SELECT GROUP_CONCAT(st0.`id` SEPARATOR ' / ') AS `breadcrumbs` FROM `tree` AS st0) AS s1 ON TRUE}
269+
end
270+
233271
test "select" do
234272
query = Schema |> select([r], {r.x, r.y}) |> plan()
235273
assert all(query) == ~s{SELECT s0.`x`, s0.`y` FROM `schema` AS s0}
@@ -608,7 +646,7 @@ defmodule Ecto.Adapters.MyXQLTest do
608646
|> plan()
609647

610648
result =
611-
"WITH `cte1` AS (SELECT s0.`id` AS `id`, ? AS `smth` FROM `schema1` AS s0 WHERE (?)), " <>
649+
"WITH `cte1` AS (SELECT ss0.`id` AS `id`, ? AS `smth` FROM `schema1` AS ss0 WHERE (?)), " <>
612650
"`cte2` AS (SELECT * FROM schema WHERE ?) " <>
613651
"SELECT s0.`id`, ? FROM `schema` AS s0 INNER JOIN `schema2` AS s1 ON ? " <>
614652
"INNER JOIN `schema2` AS s2 ON ? WHERE (?) AND (?) " <>

test/ecto/adapters/postgres_test.exs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ defmodule Ecto.Adapters.PostgresTest do
132132

133133
assert all(query) ==
134134
~s{WITH RECURSIVE "tree" AS } <>
135-
~s{(SELECT c0."id" AS "id", 1 AS "depth" FROM "categories" AS c0 WHERE (c0."parent_id" IS NULL) } <>
135+
~s{(SELECT sc0."id" AS "id", 1 AS "depth" FROM "categories" AS sc0 WHERE (sc0."parent_id" IS NULL) } <>
136136
~s{UNION ALL } <>
137137
~s{(SELECT c0."id", t1."depth" + 1 FROM "categories" AS c0 } <>
138138
~s{INNER JOIN "tree" AS t1 ON t1."id" = c0."parent_id")) } <>
@@ -171,8 +171,8 @@ defmodule Ecto.Adapters.PostgresTest do
171171

172172
assert all(query) ==
173173
~s{WITH "comments_scope" AS (} <>
174-
~s{SELECT c0."entity_id" AS "entity_id", c0."text" AS "text" } <>
175-
~s{FROM "comments" AS c0 WHERE (c0."deleted_at" IS NULL)) } <>
174+
~s{SELECT sc0."entity_id" AS "entity_id", sc0."text" AS "text" } <>
175+
~s{FROM "comments" AS sc0 WHERE (sc0."deleted_at" IS NULL)) } <>
176176
~s{SELECT p0."title", c1."text" } <>
177177
~s{FROM "posts" AS p0 } <>
178178
~s{INNER JOIN "comments_scope" AS c1 ON c1."entity_id" = p0."guid" } <>
@@ -212,7 +212,7 @@ defmodule Ecto.Adapters.PostgresTest do
212212

213213
assert update_all(query) ==
214214
~s{WITH "target_rows" AS } <>
215-
~s{(SELECT s0."id" AS "id" FROM "schema" AS s0 ORDER BY s0."id" LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
215+
~s{(SELECT ss0."id" AS "id" FROM "schema" AS ss0 ORDER BY ss0."id" LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
216216
~s{UPDATE "schema" AS s0 } <>
217217
~s{SET "x" = 123 } <>
218218
~s{FROM "target_rows" AS t1 } <>
@@ -233,13 +233,51 @@ defmodule Ecto.Adapters.PostgresTest do
233233

234234
assert delete_all(query) ==
235235
~s{WITH "target_rows" AS } <>
236-
~s{(SELECT s0."id" AS "id" FROM "schema" AS s0 ORDER BY s0."id" LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
236+
~s{(SELECT ss0."id" AS "id" FROM "schema" AS ss0 ORDER BY ss0."id" LIMIT 10 FOR UPDATE SKIP LOCKED) } <>
237237
~s{DELETE FROM "schema" AS s0 } <>
238238
~s{USING "target_rows" AS t1 } <>
239239
~s{WHERE (t1."id" = s0."id") } <>
240240
~s{RETURNING s0."id", s0."x", s0."y", s0."z", s0."w", s0."meta"}
241241
end
242242

243+
test "parent binding subquery and CTE" do
244+
initial_query =
245+
"categories"
246+
|> where([c], c.id == parent_as(:parent_category).id)
247+
|> select([:id, :parent_id])
248+
249+
iteration_query =
250+
"categories"
251+
|> join(:inner, [c], t in "tree", on: t.parent_id == c.id)
252+
|> select([:id, :parent_id])
253+
254+
cte_query = initial_query |> union_all(^iteration_query)
255+
256+
breadcrumbs_query =
257+
"tree"
258+
|> recursive_ctes(true)
259+
|> with_cte("tree", as: ^cte_query)
260+
|> select([t], %{breadcrumbs: fragment("STRING_AGG(?, ' / ')", t.id)})
261+
262+
query =
263+
from(c in "categories",
264+
as: :parent_category,
265+
left_lateral_join: b in subquery(breadcrumbs_query),
266+
select: %{id: c.id, breadcrumbs: b.breadcrumbs}
267+
)
268+
|> plan()
269+
270+
assert all(query) ==
271+
~s{SELECT c0."id", s1."breadcrumbs" FROM "categories" AS c0 } <>
272+
~s{LEFT OUTER JOIN LATERAL } <>
273+
~s{(WITH RECURSIVE "tree" AS } <>
274+
~s{(SELECT ssc0."id" AS "id", ssc0."parent_id" AS "parent_id" FROM "categories" AS ssc0 WHERE (ssc0."id" = c0."id") } <>
275+
~s{UNION ALL } <>
276+
~s{(SELECT c0."id", c0."parent_id" FROM "categories" AS c0 } <>
277+
~s{INNER JOIN "tree" AS t1 ON t1."parent_id" = c0."id")) } <>
278+
~s{SELECT STRING_AGG(st0."id", ' / ') AS "breadcrumbs" FROM "tree" AS st0) AS s1 ON TRUE}
279+
end
280+
243281
test "select" do
244282
query = Schema |> select([r], {r.x, r.y}) |> plan()
245283
assert all(query) == ~s{SELECT s0."x", s0."y" FROM "schema" AS s0}
@@ -682,7 +720,7 @@ defmodule Ecto.Adapters.PostgresTest do
682720
|> plan()
683721

684722
result =
685-
"WITH \"cte1\" AS (SELECT s0.\"id\" AS \"id\", $1 AS \"smth\" FROM \"schema1\" AS s0 WHERE ($2)), " <>
723+
"WITH \"cte1\" AS (SELECT ss0.\"id\" AS \"id\", $1 AS \"smth\" FROM \"schema1\" AS ss0 WHERE ($2)), " <>
686724
"\"cte2\" AS (SELECT * FROM schema WHERE $3) " <>
687725
"SELECT s0.\"id\", $4 FROM \"schema\" AS s0 INNER JOIN \"schema2\" AS s1 ON $5 " <>
688726
"INNER JOIN \"schema2\" AS s2 ON $6 WHERE ($7) AND ($8) " <>

0 commit comments

Comments
 (0)