diff --git a/label_studio/tasks/migrations/0056_task_prediction_result_proj_gin_idx_async.py b/label_studio/tasks/migrations/0056_task_prediction_result_proj_gin_idx_async.py new file mode 100644 index 000000000000..f9549091f683 --- /dev/null +++ b/label_studio/tasks/migrations/0056_task_prediction_result_proj_gin_idx_async.py @@ -0,0 +1,95 @@ +# Generated by Django 5.1.10 on 2025-08-07 16:14 + +import logging + +from django.conf import settings +from django.db import migrations + +from core.redis import start_job_async_or_sync +from core.models import AsyncMigrationStatus +from core.utils.common import btree_gin_migration_operations + +logger = logging.getLogger(__name__) + +IS_SQLITE = settings.DJANGO_DB == settings.DJANGO_DB_SQLITE + +migration_name = "0056_prediction_result_proj_gin_idx_async" + +SQL_CREATE_INDEX = ( + "CREATE INDEX CONCURRENTLY IF NOT EXISTS tasks_predictions_result_proj_gin " + "ON prediction USING GIN (project_id, CAST(result AS text) gin_trgm_ops);" +) + +SQL_DROP_INDEX = "DROP INDEX CONCURRENTLY IF EXISTS tasks_predictions_result_proj_gin;" + +def _forward(migration_name: str): + """Create the GIN index inside a dedicated job.""" + # If the migration has already been executed, do nothing + migration, created = AsyncMigrationStatus.objects.get_or_create( + name=migration_name, + defaults={"status": AsyncMigrationStatus.STATUS_STARTED}, + ) + if not created: + logger.info("Migration %s already executed", migration_name) + return + + logger.info("Starting async migration %s", migration_name) + from django.db import connection + + with connection.cursor() as cursor: + cursor.execute(SQL_CREATE_INDEX) + migration.status = AsyncMigrationStatus.STATUS_FINISHED + migration.save() + logger.info("Async migration %s complete", migration_name) + + +def _backward(migration_name: str): + """Revert the GIN index creation.""" + migration = AsyncMigrationStatus.objects.create( + name=migration_name, + status=AsyncMigrationStatus.STATUS_STARTED, + ) + logger.info("Reverting async migration %s", migration_name) + from django.db import connection + + with connection.cursor() as cursor: + cursor.execute(SQL_DROP_INDEX) + migration.status = AsyncMigrationStatus.STATUS_FINISHED + migration.save() + logger.info("Revert of async migration %s complete", migration_name) + + +def forwards(apps, schema_editor): + if IS_SQLITE: + logger.info("SQLite detected; skipping GIN index creation") + return + + # Only run on PostgreSQL + if not schema_editor.connection.vendor.startswith("postgres"): + logger.info("Database vendor: %s. Skipping index creation", schema_editor.connection.vendor) + return + + start_job_async_or_sync(_forward, migration_name=migration_name) + + +def backwards(apps, schema_editor): + if IS_SQLITE: + logger.info("SQLite detected; skipping GIN index drop") + return + + if not schema_editor.connection.vendor.startswith("postgres"): + logger.info("Database vendor: %s. Skipping index drop", schema_editor.connection.vendor) + return + + start_job_async_or_sync(_backward, migration_name=migration_name) + + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("tasks", "0055_task_proj_octlen_idx_async"), + ] + + operations = btree_gin_migration_operations(migrations.RunPython(forwards, backwards)) diff --git a/web/libs/datamanager/src/stores/Tabs/tab_column.jsx b/web/libs/datamanager/src/stores/Tabs/tab_column.jsx index dd2a39dd6e48..b31da3e4d54d 100644 --- a/web/libs/datamanager/src/stores/Tabs/tab_column.jsx +++ b/web/libs/datamanager/src/stores/Tabs/tab_column.jsx @@ -200,7 +200,8 @@ export const TabColumn = types get isAnnotationResultsFilterColumn() { // these columns are not visible in the column selector, but are used for filtering - return self.id.includes("annotations_results_json.") || self.id.endsWith(":annotations_results_json"); + const hidden_column_ids = ["annotations_results_json", "predictions_results_json"]; + return hidden_column_ids.some((id) => self.id.includes(`${id}.`) || self.id.endsWith(`:${id}`)); }, })) .actions((self) => ({