mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +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
						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