From f44d8828eed263851ade77614cb7facbd457fd98 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 15:56:31 -0400 Subject: [PATCH 1/3] OpenConceptLab/ocl_online#476 | Covering list indexes so deep pages walk their OFFSET in the index concepts_repo_list_idx and mappings_repo_list_idx become concepts/mappings_repo_list_ids_idx: the same key and predicate with INCLUDE (id), so a HEAD list's ids subquery (oclapi2#937) can walk a deep page's OFFSET as an index-only scan instead of reading every skipped row. Non-atomic migrations, built with CONCURRENTLY. On a table of more than 3M rows they don't build: build the index by hand first (every API task runs migrate before serving, and a build past the health check's grace leaves an INVALID index). Given a valid index, they drop the old one; given an invalid one, they stop and keep the old one. Co-Authored-By: Claude Opus 5.5 --- .../0090_concepts_repo_list_ids_idx.py | 62 +++++++++++++++++ core/integration_tests/tests_list_indexes.py | 68 +++++++++++++++++++ .../0061_mappings_repo_list_ids_idx.py | 62 +++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 core/concepts/migrations/0090_concepts_repo_list_ids_idx.py create mode 100644 core/integration_tests/tests_list_indexes.py create mode 100644 core/mappings/migrations/0061_mappings_repo_list_ids_idx.py diff --git a/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py new file mode 100644 index 000000000..b345ef2b4 --- /dev/null +++ b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py @@ -0,0 +1,62 @@ +""" +Replaces concepts_repo_list_idx with a covering index (INCLUDE id), so a deep page of a source's HEAD concepts +list can walk its OFFSET in the index alone, without reading every skipped row from the table +(OpenConceptLab/ocl_online#476). + +On a large table, build the index by hand before deploying: every API task runs `migrate` before it serves, and a +build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks the new +index and drops the old one. It builds the index itself only on a small table (a fresh, test or dev database). +""" +from django.db import migrations + +TABLE = 'concepts' +NEW_INDEX = 'concepts_repo_list_ids_idx' +OLD_INDEX = 'concepts_repo_list_idx' +PREDICATE = 'not retired and is_active and id = versioned_object_id' +CREATE_NEW_INDEX = ( + f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' + f'where {PREDICATE};' +) +CREATE_OLD_INDEX = ( + f'create index concurrently if not exists {OLD_INDEX} on {TABLE} (parent_id, updated_at desc) ' + f'where {PREDICATE};' +) +MAX_ROWS_TO_BUILD_HERE = 3_000_000 + + +def replace_index(apps, schema_editor): # pylint: disable=unused-argument + with schema_editor.connection.cursor() as cursor: + cursor.execute('select to_regclass(%s) is not null', [NEW_INDEX]) + if not cursor.fetchone()[0]: + cursor.execute('select reltuples from pg_class where oid = %s::regclass', [TABLE]) + rows = cursor.fetchone()[0] + if rows > MAX_ROWS_TO_BUILD_HERE: + raise RuntimeError( + f'{TABLE} has about {rows:,.0f} rows: build {NEW_INDEX} by hand before deploying, ' + f'then migrate again. {CREATE_NEW_INDEX}') + cursor.execute(CREATE_NEW_INDEX) + + cursor.execute('select indisvalid from pg_index where indexrelid = %s::regclass', [NEW_INDEX]) + if not cursor.fetchone()[0]: + raise RuntimeError( + f'{NEW_INDEX} is INVALID (an interrupted build). Run `drop index concurrently {NEW_INDEX};`, ' + f'build it again, then migrate again. {CREATE_NEW_INDEX}') + cursor.execute(f'drop index concurrently if exists {OLD_INDEX};') + + +def restore_index(apps, schema_editor): # pylint: disable=unused-argument + with schema_editor.connection.cursor() as cursor: + cursor.execute(CREATE_OLD_INDEX) + cursor.execute(f'drop index concurrently if exists {NEW_INDEX};') + + +class Migration(migrations.Migration): + atomic = False # CREATE/DROP INDEX CONCURRENTLY can't run in a transaction + + dependencies = [ + ('concepts', '0089_merge_20261005_0816'), + ] + + operations = [ + migrations.RunPython(replace_index, restore_index), + ] diff --git a/core/integration_tests/tests_list_indexes.py b/core/integration_tests/tests_list_indexes.py new file mode 100644 index 000000000..875f16dd1 --- /dev/null +++ b/core/integration_tests/tests_list_indexes.py @@ -0,0 +1,68 @@ +import importlib +from unittest.mock import MagicMock + +from django.db import connection +from django.test import SimpleTestCase, TestCase + +MIGRATIONS = ( + 'core.concepts.migrations.0090_concepts_repo_list_ids_idx', + 'core.mappings.migrations.0061_mappings_repo_list_ids_idx', +) + + +class ListCoveringIndexTest(TestCase): + def test_list_indexes_cover_id(self): + with connection.cursor() as cursor: + for table in ('concepts', 'mappings'): + cursor.execute( + 'select i.indexdef, x.indisvalid from pg_indexes i join pg_index x on ' + 'x.indexrelid = to_regclass(i.indexname) where i.indexname = %s', [f'{table}_repo_list_ids_idx']) + definition, valid = cursor.fetchone() + self.assertTrue(valid) + self.assertIn('(parent_id, updated_at DESC) INCLUDE (id)', definition) + self.assertIn('WHERE ((NOT retired) AND is_active AND (id = versioned_object_id))', definition) + + cursor.execute('select to_regclass(%s)', [f'{table}_repo_list_idx']) + self.assertIsNone(cursor.fetchone()[0]) + + +class ListCoveringIndexMigrationTest(SimpleTestCase): + """The migrations never build on a production-sized table, and never drop the old index for an invalid one.""" + @staticmethod + def run_migration(module, *answers): + cursor = MagicMock() + cursor.fetchone.side_effect = answers + schema_editor = MagicMock() + schema_editor.connection.cursor.return_value.__enter__.return_value = cursor + error = None + try: + importlib.import_module(module).replace_index(None, schema_editor) + except RuntimeError as ex: + error = str(ex) + return error, [call.args[0] for call in cursor.execute.call_args_list] + + def test_refuses_to_build_on_a_large_table(self): + for module in MIGRATIONS: + error, statements = self.run_migration(module, (False,), (10_000_000,)) + self.assertIn('by hand before deploying', error) + self.assertFalse([sql for sql in statements if 'create index' in sql or 'drop index' in sql]) + + def test_builds_on_a_small_table(self): + for module in MIGRATIONS: + error, statements = self.run_migration(module, (False,), (1_000,), (True,)) + self.assertIsNone(error) + self.assertIn('create index concurrently', statements[2]) + self.assertIn('drop index concurrently', statements[-1]) + + def test_uses_an_index_built_by_hand(self): + for module in MIGRATIONS: + error, statements = self.run_migration(module, (True,), (True,)) + self.assertIsNone(error) + self.assertFalse([sql for sql in statements if 'create index' in sql]) + self.assertIn('drop index concurrently', statements[-1]) + + def test_keeps_the_old_index_when_the_new_one_is_invalid(self): + for module in MIGRATIONS: + error, statements = self.run_migration(module, (True,), (False,)) + self.assertIn('INVALID', error) + self.assertFalse([sql for sql in statements if 'drop index' in sql]) diff --git a/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py new file mode 100644 index 000000000..62ead5f1e --- /dev/null +++ b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py @@ -0,0 +1,62 @@ +""" +Replaces mappings_repo_list_idx with a covering index (INCLUDE id), so a deep page of a source's HEAD mappings +list can walk its OFFSET in the index alone, without reading every skipped row from the table +(OpenConceptLab/ocl_online#476). + +On a large table, build the index by hand before deploying: every API task runs `migrate` before it serves, and a +build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks the new +index and drops the old one. It builds the index itself only on a small table (a fresh, test or dev database). +""" +from django.db import migrations + +TABLE = 'mappings' +NEW_INDEX = 'mappings_repo_list_ids_idx' +OLD_INDEX = 'mappings_repo_list_idx' +PREDICATE = 'not retired and is_active and id = versioned_object_id' +CREATE_NEW_INDEX = ( + f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' + f'where {PREDICATE};' +) +CREATE_OLD_INDEX = ( + f'create index concurrently if not exists {OLD_INDEX} on {TABLE} (parent_id, updated_at desc) ' + f'where {PREDICATE};' +) +MAX_ROWS_TO_BUILD_HERE = 3_000_000 + + +def replace_index(apps, schema_editor): # pylint: disable=unused-argument + with schema_editor.connection.cursor() as cursor: + cursor.execute('select to_regclass(%s) is not null', [NEW_INDEX]) + if not cursor.fetchone()[0]: + cursor.execute('select reltuples from pg_class where oid = %s::regclass', [TABLE]) + rows = cursor.fetchone()[0] + if rows > MAX_ROWS_TO_BUILD_HERE: + raise RuntimeError( + f'{TABLE} has about {rows:,.0f} rows: build {NEW_INDEX} by hand before deploying, ' + f'then migrate again. {CREATE_NEW_INDEX}') + cursor.execute(CREATE_NEW_INDEX) + + cursor.execute('select indisvalid from pg_index where indexrelid = %s::regclass', [NEW_INDEX]) + if not cursor.fetchone()[0]: + raise RuntimeError( + f'{NEW_INDEX} is INVALID (an interrupted build). Run `drop index concurrently {NEW_INDEX};`, ' + f'build it again, then migrate again. {CREATE_NEW_INDEX}') + cursor.execute(f'drop index concurrently if exists {OLD_INDEX};') + + +def restore_index(apps, schema_editor): # pylint: disable=unused-argument + with schema_editor.connection.cursor() as cursor: + cursor.execute(CREATE_OLD_INDEX) + cursor.execute(f'drop index concurrently if exists {NEW_INDEX};') + + +class Migration(migrations.Migration): + atomic = False # CREATE/DROP INDEX CONCURRENTLY can't run in a transaction + + dependencies = [ + ('mappings', '0060_retire_public_edit_access'), + ] + + operations = [ + migrations.RunPython(replace_index, restore_index), + ] From 6ff431e042aadbcd4f4475af12e23cb5e6e29f36 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:05:13 -0400 Subject: [PATCH 2/3] OpenConceptLab/ocl_online#476 | Only add the covering indexes; drop the old ones later Codex review of #938: - F1.1: the migrations dropped the old list indexes as soon as the new ones were valid, before anyone could confirm the new ones are used, which the ticket requires. They now only add the covering indexes; a later migration drops the old ones once EXPLAIN on staging and production shows them in use. - F1.2: an interrupted rollback could leave an invalid old index and then drop the valid covering one. Rollback now only drops the covering index. Co-Authored-By: Claude Opus 5.5 --- .../0090_concepts_repo_list_ids_idx.py | 24 +++++++------------ core/integration_tests/tests_list_indexes.py | 22 ++++++++++------- .../0061_mappings_repo_list_ids_idx.py | 24 +++++++------------ 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py index b345ef2b4..1c2f73958 100644 --- a/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py +++ b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py @@ -1,30 +1,26 @@ """ -Replaces concepts_repo_list_idx with a covering index (INCLUDE id), so a deep page of a source's HEAD concepts -list can walk its OFFSET in the index alone, without reading every skipped row from the table -(OpenConceptLab/ocl_online#476). +Adds concepts_repo_list_ids_idx: concepts_repo_list_idx's key and predicate with INCLUDE (id), so a deep page of a source's HEAD +concepts list can walk its OFFSET in the index alone, without reading every skipped row from the table +(OpenConceptLab/ocl_online#476). concepts_repo_list_idx stays until EXPLAIN on staging and production shows the new index in use; +a later migration drops it. On a large table, build the index by hand before deploying: every API task runs `migrate` before it serves, and a -build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks the new -index and drops the old one. It builds the index itself only on a small table (a fresh, test or dev database). +build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks it. It +builds the index itself only on a small table (a fresh, test or dev database). """ from django.db import migrations TABLE = 'concepts' NEW_INDEX = 'concepts_repo_list_ids_idx' -OLD_INDEX = 'concepts_repo_list_idx' PREDICATE = 'not retired and is_active and id = versioned_object_id' CREATE_NEW_INDEX = ( f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' f'where {PREDICATE};' ) -CREATE_OLD_INDEX = ( - f'create index concurrently if not exists {OLD_INDEX} on {TABLE} (parent_id, updated_at desc) ' - f'where {PREDICATE};' -) MAX_ROWS_TO_BUILD_HERE = 3_000_000 -def replace_index(apps, schema_editor): # pylint: disable=unused-argument +def add_index(apps, schema_editor): # pylint: disable=unused-argument with schema_editor.connection.cursor() as cursor: cursor.execute('select to_regclass(%s) is not null', [NEW_INDEX]) if not cursor.fetchone()[0]: @@ -41,12 +37,10 @@ def replace_index(apps, schema_editor): # pylint: disable=unused-argument raise RuntimeError( f'{NEW_INDEX} is INVALID (an interrupted build). Run `drop index concurrently {NEW_INDEX};`, ' f'build it again, then migrate again. {CREATE_NEW_INDEX}') - cursor.execute(f'drop index concurrently if exists {OLD_INDEX};') -def restore_index(apps, schema_editor): # pylint: disable=unused-argument +def drop_index(apps, schema_editor): # pylint: disable=unused-argument with schema_editor.connection.cursor() as cursor: - cursor.execute(CREATE_OLD_INDEX) cursor.execute(f'drop index concurrently if exists {NEW_INDEX};') @@ -58,5 +52,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(replace_index, restore_index), + migrations.RunPython(add_index, drop_index), ] diff --git a/core/integration_tests/tests_list_indexes.py b/core/integration_tests/tests_list_indexes.py index 875f16dd1..d12f684c6 100644 --- a/core/integration_tests/tests_list_indexes.py +++ b/core/integration_tests/tests_list_indexes.py @@ -22,12 +22,13 @@ def test_list_indexes_cover_id(self): self.assertIn('(parent_id, updated_at DESC) INCLUDE (id)', definition) self.assertIn('WHERE ((NOT retired) AND is_active AND (id = versioned_object_id))', definition) + # the old index stays until EXPLAIN in production shows the new one in use (a later migration) cursor.execute('select to_regclass(%s)', [f'{table}_repo_list_idx']) - self.assertIsNone(cursor.fetchone()[0]) + self.assertIsNotNone(cursor.fetchone()[0]) class ListCoveringIndexMigrationTest(SimpleTestCase): - """The migrations never build on a production-sized table, and never drop the old index for an invalid one.""" + """The migrations never build on a production-sized table, and never accept an invalid index.""" @staticmethod def run_migration(module, *answers): cursor = MagicMock() @@ -36,7 +37,7 @@ def run_migration(module, *answers): schema_editor.connection.cursor.return_value.__enter__.return_value = cursor error = None try: - importlib.import_module(module).replace_index(None, schema_editor) + importlib.import_module(module).add_index(None, schema_editor) except RuntimeError as ex: error = str(ex) return error, [call.args[0] for call in cursor.execute.call_args_list] @@ -45,24 +46,27 @@ def test_refuses_to_build_on_a_large_table(self): for module in MIGRATIONS: error, statements = self.run_migration(module, (False,), (10_000_000,)) self.assertIn('by hand before deploying', error) - self.assertFalse([sql for sql in statements if 'create index' in sql or 'drop index' in sql]) + self.assertFalse([sql for sql in statements if 'create index' in sql]) def test_builds_on_a_small_table(self): for module in MIGRATIONS: error, statements = self.run_migration(module, (False,), (1_000,), (True,)) self.assertIsNone(error) self.assertIn('create index concurrently', statements[2]) - self.assertIn('drop index concurrently', statements[-1]) def test_uses_an_index_built_by_hand(self): for module in MIGRATIONS: error, statements = self.run_migration(module, (True,), (True,)) self.assertIsNone(error) self.assertFalse([sql for sql in statements if 'create index' in sql]) - self.assertIn('drop index concurrently', statements[-1]) - def test_keeps_the_old_index_when_the_new_one_is_invalid(self): + def test_stops_on_an_invalid_index(self): for module in MIGRATIONS: - error, statements = self.run_migration(module, (True,), (False,)) + error, _ = self.run_migration(module, (True,), (False,)) self.assertIn('INVALID', error) - self.assertFalse([sql for sql in statements if 'drop index' in sql]) + + def test_never_drops_an_index_going_forward(self): + for module in MIGRATIONS: + for answers in (((False,), (1_000,), (True,)), ((True,), (True,)), ((True,), (False,))): + _, statements = self.run_migration(module, *answers) + self.assertFalse([sql for sql in statements if 'drop index' in sql]) diff --git a/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py index 62ead5f1e..ce6567e18 100644 --- a/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py +++ b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py @@ -1,30 +1,26 @@ """ -Replaces mappings_repo_list_idx with a covering index (INCLUDE id), so a deep page of a source's HEAD mappings -list can walk its OFFSET in the index alone, without reading every skipped row from the table -(OpenConceptLab/ocl_online#476). +Adds mappings_repo_list_ids_idx: mappings_repo_list_idx's key and predicate with INCLUDE (id), so a deep page of a source's HEAD +mappings list can walk its OFFSET in the index alone, without reading every skipped row from the table +(OpenConceptLab/ocl_online#476). mappings_repo_list_idx stays until EXPLAIN on staging and production shows the new index in use; +a later migration drops it. On a large table, build the index by hand before deploying: every API task runs `migrate` before it serves, and a -build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks the new -index and drops the old one. It builds the index itself only on a small table (a fresh, test or dev database). +build that outlasts the health check's grace leaves an INVALID index behind. This migration then only checks it. It +builds the index itself only on a small table (a fresh, test or dev database). """ from django.db import migrations TABLE = 'mappings' NEW_INDEX = 'mappings_repo_list_ids_idx' -OLD_INDEX = 'mappings_repo_list_idx' PREDICATE = 'not retired and is_active and id = versioned_object_id' CREATE_NEW_INDEX = ( f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' f'where {PREDICATE};' ) -CREATE_OLD_INDEX = ( - f'create index concurrently if not exists {OLD_INDEX} on {TABLE} (parent_id, updated_at desc) ' - f'where {PREDICATE};' -) MAX_ROWS_TO_BUILD_HERE = 3_000_000 -def replace_index(apps, schema_editor): # pylint: disable=unused-argument +def add_index(apps, schema_editor): # pylint: disable=unused-argument with schema_editor.connection.cursor() as cursor: cursor.execute('select to_regclass(%s) is not null', [NEW_INDEX]) if not cursor.fetchone()[0]: @@ -41,12 +37,10 @@ def replace_index(apps, schema_editor): # pylint: disable=unused-argument raise RuntimeError( f'{NEW_INDEX} is INVALID (an interrupted build). Run `drop index concurrently {NEW_INDEX};`, ' f'build it again, then migrate again. {CREATE_NEW_INDEX}') - cursor.execute(f'drop index concurrently if exists {OLD_INDEX};') -def restore_index(apps, schema_editor): # pylint: disable=unused-argument +def drop_index(apps, schema_editor): # pylint: disable=unused-argument with schema_editor.connection.cursor() as cursor: - cursor.execute(CREATE_OLD_INDEX) cursor.execute(f'drop index concurrently if exists {NEW_INDEX};') @@ -58,5 +52,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(replace_index, restore_index), + migrations.RunPython(add_index, drop_index), ] From b3b2b6b46282ad479801813fd9af3a9e4ae8cc18 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:15:57 -0400 Subject: [PATCH 3/3] OpenConceptLab/ocl_online#476 | Simplify: inline the index predicate, trim index tests From the simplification review of #938: the predicate is inlined into the one statement that uses it; the tests drop a mocked small-table build (creating the test database runs the real one) and a validity join (a migrated database can't hold an invalid index). Co-Authored-By: Claude Opus 5.5 --- .../migrations/0090_concepts_repo_list_ids_idx.py | 3 +-- core/integration_tests/tests_list_indexes.py | 13 ++----------- .../migrations/0061_mappings_repo_list_ids_idx.py | 3 +-- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py index 1c2f73958..df6479bf0 100644 --- a/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py +++ b/core/concepts/migrations/0090_concepts_repo_list_ids_idx.py @@ -12,10 +12,9 @@ TABLE = 'concepts' NEW_INDEX = 'concepts_repo_list_ids_idx' -PREDICATE = 'not retired and is_active and id = versioned_object_id' CREATE_NEW_INDEX = ( f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' - f'where {PREDICATE};' + 'where not retired and is_active and id = versioned_object_id;' ) MAX_ROWS_TO_BUILD_HERE = 3_000_000 diff --git a/core/integration_tests/tests_list_indexes.py b/core/integration_tests/tests_list_indexes.py index d12f684c6..c385ff45e 100644 --- a/core/integration_tests/tests_list_indexes.py +++ b/core/integration_tests/tests_list_indexes.py @@ -14,11 +14,8 @@ class ListCoveringIndexTest(TestCase): def test_list_indexes_cover_id(self): with connection.cursor() as cursor: for table in ('concepts', 'mappings'): - cursor.execute( - 'select i.indexdef, x.indisvalid from pg_indexes i join pg_index x on ' - 'x.indexrelid = to_regclass(i.indexname) where i.indexname = %s', [f'{table}_repo_list_ids_idx']) - definition, valid = cursor.fetchone() - self.assertTrue(valid) + cursor.execute('select indexdef from pg_indexes where indexname = %s', [f'{table}_repo_list_ids_idx']) + definition = cursor.fetchone()[0] self.assertIn('(parent_id, updated_at DESC) INCLUDE (id)', definition) self.assertIn('WHERE ((NOT retired) AND is_active AND (id = versioned_object_id))', definition) @@ -48,12 +45,6 @@ def test_refuses_to_build_on_a_large_table(self): self.assertIn('by hand before deploying', error) self.assertFalse([sql for sql in statements if 'create index' in sql]) - def test_builds_on_a_small_table(self): - for module in MIGRATIONS: - error, statements = self.run_migration(module, (False,), (1_000,), (True,)) - self.assertIsNone(error) - self.assertIn('create index concurrently', statements[2]) - def test_uses_an_index_built_by_hand(self): for module in MIGRATIONS: error, statements = self.run_migration(module, (True,), (True,)) diff --git a/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py index ce6567e18..d97b4c8d8 100644 --- a/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py +++ b/core/mappings/migrations/0061_mappings_repo_list_ids_idx.py @@ -12,10 +12,9 @@ TABLE = 'mappings' NEW_INDEX = 'mappings_repo_list_ids_idx' -PREDICATE = 'not retired and is_active and id = versioned_object_id' CREATE_NEW_INDEX = ( f'create index concurrently if not exists {NEW_INDEX} on {TABLE} (parent_id, updated_at desc) include (id) ' - f'where {PREDICATE};' + 'where not retired and is_active and id = versioned_object_id;' ) MAX_ROWS_TO_BUILD_HERE = 3_000_000