mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
migration: Rename 'general chat' topic to empty string topic.
Zulip now supports empty string as a valid topic name. For clients predating this feature, such messages appear in "general chat" topic. Messages sent to "general chat" are stored in the database as having a "" topic. This commit adds a migration to rename the existing "general chat" topic in the database to "". Fixes parts of #32996.
This commit is contained in:
committed by
Tim Abbott
parent
d731f0d7a8
commit
1462c8ac1b
23
zerver/migrations/0679_zerver_message_edit_history_id.py
Normal file
23
zerver/migrations/0679_zerver_message_edit_history_id.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.6 on 2025-02-26 17:25
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zerver", "0678_remove_realm_allow_edit_history"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name="message",
|
||||
index=models.Index(
|
||||
condition=models.Q(("edit_history__isnull", False)),
|
||||
fields=["id"],
|
||||
name="zerver_message_edit_history_id",
|
||||
),
|
||||
),
|
||||
]
|
@@ -0,0 +1,147 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db import connection, migrations, models, transaction
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from psycopg2.sql import SQL, Identifier
|
||||
|
||||
|
||||
def update_edit_history(message_model: type[Any]) -> None:
|
||||
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
|
||||
) -> None:
|
||||
"""Because legacy clients will be unable to distinguish the topic "general chat"
|
||||
from the "" topic (displayed as italicized "general chat"), it's helpful
|
||||
to not have both types of topics exist in an organization.
|
||||
|
||||
Further, the "general chat" topic is likely to in almost every
|
||||
case be the result of an organization that followed our advice to
|
||||
just make a "general chat" topic for topic-free chat; the new
|
||||
"general chat" feature naturally replaces that learned
|
||||
behavior.
|
||||
|
||||
So it makes sense to just consider those older "general chat"
|
||||
topics to be the same as the modern general chat topic.
|
||||
|
||||
The technical way to do that is to rewrite those topics in the
|
||||
database to be represented as `""` rather than "general chat",
|
||||
since we've endeavored to make the distinction between those two
|
||||
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")
|
||||
Message = apps.get_model("zerver", "Message")
|
||||
UserTopic = apps.get_model("zerver", "UserTopic")
|
||||
ArchivedMessage = apps.get_model("zerver", "ArchivedMessage")
|
||||
ScheduledMessage = apps.get_model("zerver", "ScheduledMessage")
|
||||
|
||||
for realm in Realm.objects.all():
|
||||
with transaction.atomic(durable=True):
|
||||
# Uses index "zerver_message_realm_upper_subject"
|
||||
message_queryset = Message.objects.filter(realm=realm, subject__iexact="general chat")
|
||||
channel_ids = list(
|
||||
message_queryset.distinct("recipient__type_id").values_list(
|
||||
"recipient__type_id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
message_queryset.update(subject="")
|
||||
|
||||
# Limiting the UserTopic query to only those channels that
|
||||
# contain an actual general chat topic does not guaranteed
|
||||
# updating all UserTopic rows, since it's possible to
|
||||
# follow/mute an empty topic. But it does guarantee that
|
||||
# we update all rows that have any current effect.
|
||||
#
|
||||
# Uses index "zerver_mutedtopic_stream_topic"
|
||||
UserTopic.objects.filter(
|
||||
stream_id__in=channel_ids, topic_name__iexact="general chat"
|
||||
).update(topic_name="")
|
||||
|
||||
ArchivedMessage.objects.filter(realm=realm, subject__iexact="general chat").update(
|
||||
subject=""
|
||||
)
|
||||
ScheduledMessage.objects.filter(realm=realm, subject__iexact="general chat").update(
|
||||
subject=""
|
||||
)
|
||||
|
||||
for message_model in [Message, ArchivedMessage]:
|
||||
update_edit_history(message_model)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""
|
||||
Zulip now supports empty string as a valid topic name.
|
||||
For clients predating this feature, such messages appear
|
||||
in "general chat" topic. Messages sent to "general chat" are
|
||||
stored in the database as having a "" topic. This migration
|
||||
renames the existing "general chat" topic in the database to "".
|
||||
"""
|
||||
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("zerver", "0679_zerver_message_edit_history_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
rename_general_chat_to_empty_string_topic,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
@@ -163,7 +163,7 @@ class Message(AbstractMessage):
|
||||
|
||||
# Name to be used for the empty topic with clients that have not
|
||||
# yet migrated to have the `empty_topic_name` client capability.
|
||||
EMPTY_TOPIC_FALLBACK_NAME = "test general chat"
|
||||
EMPTY_TOPIC_FALLBACK_NAME = "general chat"
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
@@ -238,6 +238,12 @@ class Message(AbstractMessage):
|
||||
F("id").desc(nulls_last=True),
|
||||
name="zerver_message_realm_id",
|
||||
),
|
||||
models.Index(
|
||||
# Used by 0680_rename_general_chat_to_empty_string_topic
|
||||
fields=["id"],
|
||||
condition=Q(edit_history__isnull=False),
|
||||
name="zerver_message_edit_history_id",
|
||||
),
|
||||
]
|
||||
|
||||
def topic_name(self) -> str:
|
||||
|
Reference in New Issue
Block a user