mirror of
https://github.com/zulip/zulip.git
synced 2025-10-25 00:53:56 +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
|
# Name to be used for the empty topic with clients that have not
|
||||||
# yet migrated to have the `empty_topic_name` client capability.
|
# 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:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
@@ -238,6 +238,12 @@ class Message(AbstractMessage):
|
|||||||
F("id").desc(nulls_last=True),
|
F("id").desc(nulls_last=True),
|
||||||
name="zerver_message_realm_id",
|
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:
|
def topic_name(self) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user