mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +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