+ defp restrict_hashtag(query, opts) do
+ [tag_any, tag_all, tag_reject] =
+ [:tag, :tag_all, :tag_reject]
+ |> Enum.map(&opts[&1])
+ |> Enum.map(&List.wrap(&1))
+
+ has_conditions = Enum.any?([tag_any, tag_all, tag_reject], &Enum.any?(&1))
+
+ cond do
+ !has_conditions ->
+ query
+
+ opts[:skip_preload] ->
+ raise_on_missing_preload()
+
+ true ->
+ query
+ |> group_by_all_bindings()
+ |> join(:left, [_activity, object], hashtag in assoc(object, :hashtags), as: :hashtag)
+ |> maybe_restrict_hashtag_any(tag_any)
+ |> maybe_restrict_hashtag_all(tag_all)
+ |> maybe_restrict_hashtag_reject_any(tag_reject)
+ end
+ end
+
+ # Groups by all bindings to allow aggregation on hashtags
+ defp group_by_all_bindings(query) do
+ # Expecting named bindings: :object, :bookmark, :thread_mute, :report_note
+ cond do
+ Enum.count(query.aliases) == 4 ->
+ from([a, o, b3, b4, b5] in query, group_by: [a.id, o.id, b3.id, b4.id, b5.id])
+
+ Enum.count(query.aliases) == 3 ->
+ from([a, o, b3, b4] in query, group_by: [a.id, o.id, b3.id, b4.id])
+
+ Enum.count(query.aliases) == 2 ->
+ from([a, o, b3] in query, group_by: [a.id, o.id, b3.id])
+
+ true ->
+ from([a, o] in query, group_by: [a.id, o.id])
+ end
+ end
+
+ defp maybe_restrict_hashtag_any(query, []) do
+ query
+ end
+
+ defp maybe_restrict_hashtag_any(query, tags) do
+ having(
+ query,
+ [hashtag: hashtag],
+ fragment("array_agg(?) && (?)", hashtag.name, ^tags)
+ )
+ end
+
+ defp maybe_restrict_hashtag_all(query, []) do
+ query
+ end
+
+ defp maybe_restrict_hashtag_all(query, tags) do
+ having(
+ query,
+ [hashtag: hashtag],
+ fragment("array_agg(?) @> (?)", hashtag.name, ^tags)
+ )
+ end
+
+ defp maybe_restrict_hashtag_reject_any(query, []) do
+ query
+ end
+
+ defp maybe_restrict_hashtag_reject_any(query, tags) do
+ having(
+ query,
+ [hashtag: hashtag],
+ fragment("not(array_agg(?) && (?))", hashtag.name, ^tags)
+ )
+ end
+