summaryrefslogtreecommitdiff
path: root/priv/repo/migrations/20211218181632_change_object_id_to_flake.exs
blob: dd8912b939cc9d031b313ee31070edb7d1b9efaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
defmodule Pleroma.Repo.Migrations.ChangeObjectIdToFlake do
  @moduledoc """
  Convert object IDs to FlakeIds.
  Fortunately only a few tables have a foreign key to objects. Update them.
  """
  use Ecto.Migration
  require Integer

  alias Pleroma.Clippy
  alias Pleroma.Repo

  import Ecto.Query

  @delete_duplicate_ap_id_objects_query """
  DELETE FROM objects
  WHERE id IN (
    SELECT
        id
    FROM (
        SELECT
            id,
            row_number() OVER w as rnum
        FROM objects
        WHERE data->>'id' IS NOT NULL
        WINDOW w AS (
            PARTITION BY data->>'id'
            ORDER BY id
        )
    ) t
  WHERE t.rnum > 1)
  """

  @convert_objects_int_ids_to_flake_ids_query """
  alter table objects
  drop constraint objects_pkey cascade,
  alter column id drop default,
  alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid),
  add primary key (id)
  """

  def up do
    clippy = start_clippy_heartbeats()

    # Lock tables to avoid a running server meddling with our transaction
    execute("LOCK TABLE objects")
    execute("LOCK TABLE data_migration_failed_ids")
    execute("LOCK TABLE chat_message_references")
    execute("LOCK TABLE deliveries")
    execute("LOCK TABLE hashtags_objects")

    # Switch object IDs to FlakeIds
    execute(fn ->
      try do
        repo().query!(@convert_objects_int_ids_to_flake_ids_query)
      rescue
        e in Postgrex.Error ->
          # Handling of error 23505, "unique_violation": https://git.pleroma.social/pleroma/pleroma/-/issues/2771
          with %{postgres: %{pg_code: "23505"}} <- e do
            repo().query!(@delete_duplicate_ap_id_objects_query)
            repo().query!(@convert_objects_int_ids_to_flake_ids_query)
          else
            _ -> raise e
          end
      end
    end)

    # Update data_migration_failed_ids
    execute("""
    alter table data_migration_failed_ids
    drop constraint data_migration_failed_ids_pkey cascade,
    alter column record_id set data type uuid using cast( lpad( to_hex(record_id), 32, '0') as uuid),
    add primary key (data_migration_id, record_id)
    """)

    # Update chat message foreign key
    execute("""
    alter table chat_message_references
    alter column object_id set data type uuid using cast( lpad( to_hex(object_id), 32, '0') as uuid),
    add constraint chat_message_references_object_id_fkey foreign key (object_id) references objects(id) on delete cascade
    """)

    # Update delivery foreign key
    execute("""
    alter table deliveries
    alter column object_id set data type uuid using cast( lpad( to_hex(object_id), 32, '0') as uuid),
    add constraint deliveries_object_id_fkey foreign key (object_id) references objects(id) on delete cascade
    """)

    # Update hashtag many-to-many foreign key
    execute("""
    alter table hashtags_objects
    alter column object_id set data type uuid using cast( lpad( to_hex(object_id), 32, '0') as uuid),
    add constraint hashtags_objects_object_id_fkey foreign key (object_id) references objects(id) on delete cascade
    """)

    flush()

    stop_clippy_heartbeats(clippy)
  end

  def down do
    raise "This migration can't be reversed"
  end

  defp start_clippy_heartbeats() do
    count = from(o in "objects", select: count(o.id)) |> Repo.one!()

    if count > 5000 do
      heartbeat_interval = :timer.minutes(2) + :timer.seconds(30)

      all_tips =
        Clippy.tips() ++
          [
            "The migration is still running, maybe it's time for another “tea”?",
            "Happy rabbits practice a cute behavior known as a\n“binky:” they jump up in the air\nand twist\nand spin around!",
            "Nothing and everything.\n\nI still work.",
            "Pleroma runs on a Raspberry Pi!\n\n  … but this migration will take forever if you\nactually run on a raspberry pi",
            "Status? Stati? Post? Note? Toot?\nRepeat? Reboost? Boost? Retweet? Retoot??\n\nI-I'm confused."
          ]

      heartbeat = fn heartbeat, runs, all_tips, tips ->
        tips =
          if Integer.is_even(runs) do
            tips = if tips == [], do: all_tips, else: tips
            [tip | tips] = Enum.shuffle(tips)
            Clippy.puts(tip)
            tips
          else
            IO.puts(
              "\n -- #{DateTime.to_string(DateTime.utc_now())} Migration still running, please wait…\n"
            )

            tips
          end

        :timer.sleep(heartbeat_interval)
        heartbeat.(heartbeat, runs + 1, all_tips, tips)
      end

      Clippy.puts([
        [:red, :bright, "It looks like you are running an older instance!"],
        [""],
        [:bright, "This migration may take a long time", :reset, " -- so you probably should"],
        ["go drink a cofe, or a tea, or a beer, a whiskey, a vodka,"],
        ["while it runs to deal with your temporary fediverse pause!"]
      ])

      :timer.sleep(heartbeat_interval)
      spawn_link(fn -> heartbeat.(heartbeat, 1, all_tips, []) end)
    end
  end

  defp stop_clippy_heartbeats(pid) do
    if pid do
      Process.unlink(pid)
      Process.exit(pid, :kill)
      Clippy.puts([[:green, :bright, "Hurray!!", "", "", "Migration completed!"]])
    end
  end
end