[#3213] Fixed `hashtags.name` lookup (must use `citext` type to do index scan). Fixed...
[akkoma] / lib / pleroma / hashtag.ex
index b05927563d0f312c51851fa7696629cd2416bd64..a6d033816191498af1a47085f0e063458cbf3ae4 100644 (file)
@@ -6,23 +6,25 @@ defmodule Pleroma.Hashtag do
   use Ecto.Schema
 
   import Ecto.Changeset
+  import Ecto.Query
 
+  alias Ecto.Multi
   alias Pleroma.Hashtag
+  alias Pleroma.Object
   alias Pleroma.Repo
 
-  @derive {Jason.Encoder, only: [:data]}
-
   schema "hashtags" do
     field(:name, :string)
-    field(:data, :map, default: %{})
 
-    many_to_many(:objects, Pleroma.Object, join_through: "hashtags_objects", on_replace: :delete)
+    many_to_many(:objects, Object, join_through: "hashtags_objects", on_replace: :delete)
 
     timestamps()
   end
 
   def get_by_name(name) do
-    Repo.get_by(Hashtag, name: name)
+    from(h in Hashtag)
+    |> where([h], fragment("name = ?::citext", ^String.downcase(name)))
+    |> Repo.one()
   end
 
   def get_or_create_by_name(name) when is_bitstring(name) do
@@ -37,22 +39,66 @@ defmodule Pleroma.Hashtag do
   end
 
   def get_or_create_by_names(names) when is_list(names) do
-    Enum.reduce_while(names, {:ok, []}, fn name, {:ok, list} ->
-      case get_or_create_by_name(name) do
-        {:ok, %Hashtag{} = hashtag} ->
-          {:cont, {:ok, list ++ [hashtag]}}
+    names = Enum.map(names, &String.downcase/1)
+    timestamp = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+
+    structs =
+      Enum.map(names, fn name ->
+        %Hashtag{}
+        |> changeset(%{name: name})
+        |> Map.get(:changes)
+        |> Map.merge(%{inserted_at: timestamp, updated_at: timestamp})
+      end)
 
-        error ->
-          {:halt, error}
+    try do
+      with {:ok, %{query_op: hashtags}} <-
+             Multi.new()
+             |> Multi.insert_all(:insert_all_op, Hashtag, structs, on_conflict: :nothing)
+             |> Multi.run(:query_op, fn _repo, _changes ->
+               {:ok,
+                Repo.all(from(ht in Hashtag, where: ht.name in fragment("?::citext[]", ^names)))}
+             end)
+             |> Repo.transaction() do
+        {:ok, hashtags}
+      else
+        {:error, _name, value, _changes_so_far} -> {:error, value}
       end
-    end)
+    rescue
+      e -> {:error, e}
+    end
   end
 
   def changeset(%Hashtag{} = struct, params) do
     struct
-    |> cast(params, [:name, :data])
+    |> cast(params, [:name])
     |> update_change(:name, &String.downcase/1)
     |> validate_required([:name])
     |> unique_constraint(:name)
   end
+
+  def unlink(%Object{id: object_id}) do
+    with {_, hashtag_ids} <-
+           from(hto in "hashtags_objects",
+             where: hto.object_id == ^object_id,
+             select: hto.hashtag_id
+           )
+           |> Repo.delete_all(),
+         {:ok, unreferenced_count} <- delete_unreferenced(hashtag_ids) do
+      {:ok, length(hashtag_ids), unreferenced_count}
+    end
+  end
+
+  @delete_unreferenced_query """
+  DELETE FROM hashtags WHERE id IN
+    (SELECT hashtags.id FROM hashtags
+      LEFT OUTER JOIN hashtags_objects
+        ON hashtags_objects.hashtag_id = hashtags.id
+      WHERE hashtags_objects.hashtag_id IS NULL AND hashtags.id = ANY($1));
+  """
+
+  def delete_unreferenced(ids) do
+    with {:ok, %{num_rows: deleted_count}} <- Repo.query(@delete_unreferenced_query, [ids]) do
+      {:ok, deleted_count}
+    end
+  end
 end