mirror of
https://github.com/zulip/zulip.git
synced 2025-10-25 00:53:56 +00:00
migrations: Update rename_general_chat_to_empty_string_topic migration.
Earlier, the migration code was replacing the occurrence of "general chat" as topic name in the database with `""`. This resulted in an error which permanently broke `/near/` URL links to existing topics named "general chat". This commit updates the migration approach to instead move messages from "general chat" to `""`. An edit history entry is added. It results in the same expected behaviour and fixing that bug.
This commit is contained in:
committed by
Tim Abbott
parent
2f8a46ed57
commit
dd181bd03f
@@ -1,9 +1,50 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db import connection, migrations, models, transaction
|
import orjson
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import connection, migrations, transaction
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
from django.db.migrations.state import StateApps
|
from django.db.migrations.state import StateApps
|
||||||
from psycopg2.sql import SQL, Identifier, Literal
|
from django.db.models import F, Func, JSONField, TextField, Value
|
||||||
|
from django.db.models.functions import Cast
|
||||||
|
from django.utils.timezone import now as timezone_now
|
||||||
|
from psycopg2.sql import SQL, Literal
|
||||||
|
|
||||||
|
LAST_EDIT_TIME = timezone_now()
|
||||||
|
LAST_EDIT_TIMESTAMP = int(LAST_EDIT_TIME.timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
def update_messages_for_topic_edit(message_queryset: Any, notification_bot: Any) -> None:
|
||||||
|
edit_history_entry = {
|
||||||
|
"user_id": notification_bot.id,
|
||||||
|
"timestamp": LAST_EDIT_TIMESTAMP,
|
||||||
|
"prev_topic": "general chat",
|
||||||
|
"topic": "",
|
||||||
|
}
|
||||||
|
update_fields: dict[str, object] = {
|
||||||
|
"subject": "",
|
||||||
|
"last_edit_time": LAST_EDIT_TIME,
|
||||||
|
"edit_history": Cast(
|
||||||
|
Func(
|
||||||
|
Cast(
|
||||||
|
Value(orjson.dumps([edit_history_entry]).decode()),
|
||||||
|
JSONField(),
|
||||||
|
),
|
||||||
|
Cast(
|
||||||
|
Func(
|
||||||
|
F("edit_history"),
|
||||||
|
Value("[]"),
|
||||||
|
function="COALESCE",
|
||||||
|
),
|
||||||
|
JSONField(),
|
||||||
|
),
|
||||||
|
function="",
|
||||||
|
arg_joiner=" || ",
|
||||||
|
),
|
||||||
|
TextField(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
message_queryset.update(**update_fields)
|
||||||
|
|
||||||
|
|
||||||
def update_user_topic(channel_ids: list[int], user_topic_model: type[Any]) -> None:
|
def update_user_topic(channel_ids: list[int], user_topic_model: type[Any]) -> None:
|
||||||
@@ -32,59 +73,7 @@ def update_user_topic(channel_ids: list[int], user_topic_model: type[Any]) -> No
|
|||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
def update_edit_history(message_model: type[Any]) -> None:
|
def move_messages_from_general_chat_to_empty_string_topic(
|
||||||
BATCH_SIZE = 10000
|
|
||||||
lower_id_bound = 0
|
|
||||||
|
|
||||||
max_id = message_model.objects.aggregate(models.Max("id"))["id__max"]
|
|
||||||
if max_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
while lower_id_bound < max_id:
|
|
||||||
upper_id_bound = min(lower_id_bound + BATCH_SIZE, max_id)
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
query = SQL(
|
|
||||||
"""
|
|
||||||
UPDATE {table_name}
|
|
||||||
SET edit_history = (
|
|
||||||
SELECT JSONB_AGG(
|
|
||||||
elem
|
|
||||||
||
|
|
||||||
(CASE
|
|
||||||
WHEN elem ? 'prev_topic' AND elem->>'prev_topic' = 'general chat'
|
|
||||||
THEN '{{"prev_topic": ""}}'::jsonb
|
|
||||||
ELSE '{{}}'::jsonb
|
|
||||||
END)
|
|
||||||
||
|
|
||||||
(CASE
|
|
||||||
WHEN elem ? 'topic' AND elem->>'topic' = 'general chat'
|
|
||||||
THEN '{{"topic": ""}}'::jsonb
|
|
||||||
ELSE '{{}}'::jsonb
|
|
||||||
END)
|
|
||||||
)::text
|
|
||||||
FROM JSONB_ARRAY_ELEMENTS(edit_history::jsonb) AS elem
|
|
||||||
)
|
|
||||||
WHERE edit_history IS NOT NULL
|
|
||||||
AND id > %(lower_id_bound)s AND id <= %(upper_id_bound)s
|
|
||||||
AND (
|
|
||||||
edit_history::jsonb @> '[{{"prev_topic": "general chat"}}]' OR
|
|
||||||
edit_history::jsonb @> '[{{"topic": "general chat"}}]'
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
).format(table_name=Identifier(message_model._meta.db_table))
|
|
||||||
cursor.execute(
|
|
||||||
query,
|
|
||||||
{
|
|
||||||
"lower_id_bound": lower_id_bound,
|
|
||||||
"upper_id_bound": upper_id_bound,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Processed {upper_id_bound} / {max_id}")
|
|
||||||
lower_id_bound += BATCH_SIZE
|
|
||||||
|
|
||||||
|
|
||||||
def rename_general_chat_to_empty_string_topic(
|
|
||||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Because legacy clients will be unable to distinguish the topic "general chat"
|
"""Because legacy clients will be unable to distinguish the topic "general chat"
|
||||||
@@ -100,20 +89,25 @@ def rename_general_chat_to_empty_string_topic(
|
|||||||
So it makes sense to just consider those older "general chat"
|
So it makes sense to just consider those older "general chat"
|
||||||
topics to be the same as the modern general chat topic.
|
topics to be the same as the modern general chat topic.
|
||||||
|
|
||||||
The technical way to do that is to rewrite those topics in the
|
The technical way to do that is to move messages in "general chat"
|
||||||
database to be represented as `""` rather than "general chat",
|
topics to `""`, since we've endeavored to make the distinction between
|
||||||
since we've endeavored to make the distinction between those two
|
those two storage approaches invisible to legacy clients at the API layer.
|
||||||
storage approaches invisible to legacy clients at the API layer.
|
|
||||||
|
|
||||||
Thus, we don't generate edit history entries for this, since we're
|
|
||||||
thinking of it as redefining how "general chat" is stored in the
|
|
||||||
database.
|
|
||||||
"""
|
"""
|
||||||
Realm = apps.get_model("zerver", "Realm")
|
Realm = apps.get_model("zerver", "Realm")
|
||||||
Message = apps.get_model("zerver", "Message")
|
Message = apps.get_model("zerver", "Message")
|
||||||
UserTopic = apps.get_model("zerver", "UserTopic")
|
UserTopic = apps.get_model("zerver", "UserTopic")
|
||||||
ArchivedMessage = apps.get_model("zerver", "ArchivedMessage")
|
ArchivedMessage = apps.get_model("zerver", "ArchivedMessage")
|
||||||
ScheduledMessage = apps.get_model("zerver", "ScheduledMessage")
|
ScheduledMessage = apps.get_model("zerver", "ScheduledMessage")
|
||||||
|
UserProfile = apps.get_model("zerver", "UserProfile")
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_realm = Realm.objects.get(string_id=settings.SYSTEM_BOT_REALM)
|
||||||
|
except Realm.DoesNotExist:
|
||||||
|
# Server not initialized.
|
||||||
|
return
|
||||||
|
notification_bot = UserProfile.objects.get(
|
||||||
|
email__iexact=settings.NOTIFICATION_BOT, realm=internal_realm
|
||||||
|
)
|
||||||
|
|
||||||
for realm in Realm.objects.all():
|
for realm in Realm.objects.all():
|
||||||
with transaction.atomic(durable=True):
|
with transaction.atomic(durable=True):
|
||||||
@@ -125,7 +119,7 @@ def rename_general_chat_to_empty_string_topic(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
message_queryset.update(subject="")
|
update_messages_for_topic_edit(message_queryset, notification_bot)
|
||||||
|
|
||||||
# Limiting the UserTopic query to only those channels that
|
# Limiting the UserTopic query to only those channels that
|
||||||
# contain an actual general chat topic does not guaranteed
|
# contain an actual general chat topic does not guaranteed
|
||||||
@@ -134,16 +128,16 @@ def rename_general_chat_to_empty_string_topic(
|
|||||||
# we update all rows that have any current effect.
|
# we update all rows that have any current effect.
|
||||||
update_user_topic(channel_ids, UserTopic)
|
update_user_topic(channel_ids, UserTopic)
|
||||||
|
|
||||||
ArchivedMessage.objects.filter(realm=realm, subject__iexact="general chat").update(
|
# Uses index zerver_archivedmessage_realm_id_fab86889
|
||||||
subject=""
|
archived_message_queryset = ArchivedMessage.objects.filter(
|
||||||
|
realm=realm, subject__iexact="general chat"
|
||||||
)
|
)
|
||||||
|
update_messages_for_topic_edit(archived_message_queryset, notification_bot)
|
||||||
|
|
||||||
ScheduledMessage.objects.filter(realm=realm, subject__iexact="general chat").update(
|
ScheduledMessage.objects.filter(realm=realm, subject__iexact="general chat").update(
|
||||||
subject=""
|
subject=""
|
||||||
)
|
)
|
||||||
|
|
||||||
for message_model in [Message, ArchivedMessage]:
|
|
||||||
update_edit_history(message_model)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
"""
|
"""
|
||||||
@@ -151,7 +145,7 @@ class Migration(migrations.Migration):
|
|||||||
For clients predating this feature, such messages appear
|
For clients predating this feature, such messages appear
|
||||||
in "general chat" topic. Messages sent to "general chat" are
|
in "general chat" topic. Messages sent to "general chat" are
|
||||||
stored in the database as having a "" topic. This migration
|
stored in the database as having a "" topic. This migration
|
||||||
renames the existing "general chat" topic in the database to "".
|
moves the messages from "general chat" topic to `""`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
atomic = False
|
atomic = False
|
||||||
@@ -162,7 +156,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
rename_general_chat_to_empty_string_topic,
|
move_messages_from_general_chat_to_empty_string_topic,
|
||||||
reverse_code=migrations.RunPython.noop,
|
reverse_code=migrations.RunPython.noop,
|
||||||
elidable=True,
|
elidable=True,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user