diff --git a/api/app_analytics/migrations/0006_add_labels.py b/api/app_analytics/migrations/0006_add_labels.py index e3cff2d6d1b7..abd64edc0e6d 100644 --- a/api/app_analytics/migrations/0006_add_labels.py +++ b/api/app_analytics/migrations/0006_add_labels.py @@ -1,9 +1,10 @@ # Generated by Django 4.2.21 on 2025-06-16 16:55 -import django.contrib.postgres.fields.hstore -from django.contrib.postgres.operations import HStoreExtension +# import django.contrib.postgres.fields.hstore +# from django.contrib.postgres.operations import HStoreExtension from django.db import migrations +from django.db import models class Migration(migrations.Migration): @@ -13,25 +14,37 @@ class Migration(migrations.Migration): ] operations = [ - HStoreExtension(), + # # The extension usage is annulated by the subsequent migration. + # # To avoid the operational overhead of enabling it in some environments, + # # we do not create it here anymore. + # # However, leave it commented out for reference/history. + # HStoreExtension(), migrations.AddField( model_name="apiusagebucket", name="labels", - field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + # This field is now a JSONField instead of HStoreField. + # field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + field=models.JSONField(default=dict), ), migrations.AddField( model_name="apiusageraw", name="labels", - field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + # This field is now a JSONField instead of HStoreField. + # field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + field=models.JSONField(default=dict), ), migrations.AddField( model_name="featureevaluationbucket", name="labels", - field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + # This field is now a JSONField instead of HStoreField. + # field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + field=models.JSONField(default=dict), ), migrations.AddField( model_name="featureevaluationraw", name="labels", - field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + # This field is now a JSONField instead of HStoreField. + # field=django.contrib.postgres.fields.hstore.HStoreField(default=dict), + field=models.JSONField(default=dict), ), ] diff --git a/api/app_analytics/migrations/0007_labels_jsonb.py b/api/app_analytics/migrations/0007_labels_jsonb.py new file mode 100644 index 000000000000..002323d30524 --- /dev/null +++ b/api/app_analytics/migrations/0007_labels_jsonb.py @@ -0,0 +1,73 @@ +# Generated by Django 4.2.22 on 2025-07-25 14:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("app_analytics", "0006_add_labels"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="apiusagebucket", + name="labels", + field=models.JSONField(default=dict), + ), + migrations.AlterField( + model_name="apiusageraw", + name="labels", + field=models.JSONField(default=dict), + ), + migrations.AlterField( + model_name="featureevaluationbucket", + name="labels", + field=models.JSONField(default=dict), + ), + migrations.AlterField( + model_name="featureevaluationraw", + name="labels", + field=models.JSONField(default=dict), + ), + ], + database_operations=[ + migrations.RunSQL( + # Only alter the columns that are currently hstore. + # See 0006_add_labels to understand why we are doing this. + sql=""" + DO $$ + BEGIN + FOR relname IN + SELECT c.relname + FROM pg_class c + JOIN pg_attribute a ON a.attrelid = c.oid + JOIN pg_type t ON a.atttypid = t.oid + WHERE c.relname IN ( + 'app_analytics_apiusagebucket', + 'app_analytics_apiusageraw', + 'app_analytics_featureevaluationbucket', + 'app_analytics_featureevaluationraw' + ) + AND a.attname = 'labels' + AND t.typname = 'hstore' + LOOP + EXECUTE format( + 'ALTER TABLE %I + ALTER COLUMN labels TYPE jsonb USING hstore_to_json(labels), + ALTER COLUMN labels SET DEFAULT ''{}''::jsonb', + relname + ); + END LOOP; + END + $$; + """, + # We don't want hstore in the database at all, + # so don't do anything for reverse SQL. + reverse_sql="", + ), + ], + ), + ] diff --git a/api/app_analytics/models.py b/api/app_analytics/models.py index aa55e2e17cbc..72ba2c957608 100644 --- a/api/app_analytics/models.py +++ b/api/app_analytics/models.py @@ -1,6 +1,5 @@ from datetime import timedelta -from django.contrib.postgres.fields import HStoreField from django.core.exceptions import ValidationError from django.db import models from django_lifecycle import ( # type: ignore[import-untyped] @@ -59,7 +58,7 @@ class APIUsageRaw(models.Model): host = models.CharField(max_length=255) resource = models.IntegerField(choices=Resource.choices) count = models.PositiveIntegerField(default=1) - labels = HStoreField(default=dict) + labels = models.JSONField(default=dict) class Meta: index_together = (("environment_id", "created_at"),) @@ -70,7 +69,7 @@ class AbstractBucket(LifecycleModelMixin, models.Model): # type: ignore[misc] created_at = models.DateTimeField() total_count = models.PositiveIntegerField() environment_id = models.PositiveIntegerField() - labels = HStoreField(default=dict) + labels = models.JSONField(default=dict) class Meta: abstract = True @@ -107,7 +106,7 @@ class FeatureEvaluationRaw(models.Model): environment_id = models.PositiveIntegerField() evaluation_count = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) - labels = HStoreField(default=dict) + labels = models.JSONField(default=dict) # Both stored for tracking multivariate split testing. identity_identifier = models.CharField(max_length=2000, null=True, default=None)