Skip to content

Commit 7fa870c

Browse files
authored
Ensure that m2m field indexes are created when using add_field (baserow#4491)
1 parent 8c0d3bc commit 7fa870c

File tree

6 files changed

+101
-0
lines changed

6 files changed

+101
-0
lines changed

backend/src/baserow/contrib/database/db/schema.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,41 @@ def delete_model(self, model):
355355
):
356356
self.deferred_sql.remove(sql)
357357

358+
def ensure_single_column_index(self, model, field):
359+
"""
360+
Ensure an index exists on model(field.column). Safe to call repeatedly.
361+
"""
362+
363+
# If any index/constraint exists that is backed by an index on exactly this
364+
# column, don't create another.
365+
if self._constraint_names(model, [field.column], index=True):
366+
return
367+
368+
stmt = self._create_index_sql(model, fields=[field])
369+
self.execute(stmt, params=None)
370+
371+
def ensure_m2m_field_indexes(self, field):
372+
if field.many_to_many and field.remote_field.through._meta.auto_created:
373+
through = field.remote_field.through
374+
# Ensure the two FK indexes exist (especially important for the "reverse"
375+
# FK). m2m_field_name() / m2m_reverse_field_name() return the
376+
# through-field names.
377+
for through_field_name in (
378+
field.m2m_field_name(),
379+
field.m2m_reverse_field_name(),
380+
):
381+
fk_field = through._meta.get_field(through_field_name)
382+
self.ensure_single_column_index(through, fk_field)
383+
384+
def add_field(self, model, field):
385+
return_value = super().add_field(model, field)
386+
# Using the `create_model` to create a Baserow table, like what we do on
387+
# `import_serialize` does actually create the indexes of the through table.
388+
# However, when using `add_field` it does not. The code below will make sure
389+
# that the needed indexes are created.
390+
self.ensure_m2m_field_indexes(field)
391+
return return_value
392+
358393

359394
@contextlib.contextmanager
360395
def safe_django_schema_editor(atomic=True, name=None, classes=None, **kwargs):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 5.0.14 on 2025-12-19 21:52
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("database", "0201_increase_pendingsearchvalueupdate_statistics"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="table",
14+
name="missing_m2m_indexes_added",
15+
field=models.BooleanField(
16+
db_default=False,
17+
default=True,
18+
help_text="Indicates whether potentially missing m2m foreign key indexes have been added.",
19+
),
20+
),
21+
]

backend/src/baserow/contrib/database/table/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,20 @@ class Table(
889889
null=True,
890890
help_text="Indicates whether the table has had the field_rules_are_valid column added.",
891891
)
892+
# The m2m indexes of the foreign keys were not added before because the
893+
# `schema_editor.add_field` does not add them. The `schema_editor.create_model`
894+
# does add those. This problem has been addressed, but there are tables out there
895+
# without those indexes.
896+
missing_m2m_indexes_added = models.BooleanField(
897+
# The `db_default` must be false because this is used when an entry is created
898+
# no default value is set. This is what happens when the field index changes
899+
# are not yet deployed, so index does not exist.
900+
db_default=False,
901+
# However, if the field index changes are deployed, this default value is used,
902+
# and in that case, the index has been applied.
903+
default=True,
904+
help_text="Indicates whether potentially missing m2m foreign key indexes have been added.",
905+
)
892906

893907
class Meta:
894908
ordering = ("order",)

backend/src/baserow/contrib/database/table/tasks.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,25 @@ def setup_created_by_and_last_modified_by_column(self, table_id: int):
127127
TableHandler().create_created_by_and_last_modified_by_fields(table)
128128

129129

130+
@app.task(bind=True, queue="export")
131+
def setup_m2m_field_indexes_if_not_exist(self, table_id: int):
132+
from baserow.contrib.database.db.schema import safe_django_schema_editor
133+
from baserow.contrib.database.table.handler import TableHandler
134+
135+
with transaction.atomic():
136+
table = TableHandler().get_table_for_update(table_id)
137+
model = table.get_model()
138+
fields = model.get_fields()
139+
140+
with safe_django_schema_editor(atomic=False) as schema_editor:
141+
for field in fields:
142+
model_field = model._meta.get_field(field.db_column)
143+
schema_editor.ensure_m2m_field_indexes(model_field)
144+
145+
table.missing_m2m_indexes_added = True
146+
table.save(update_fields=["missing_m2m_indexes_added"])
147+
148+
130149
@app.task(bind=True)
131150
def update_table_usage(self, table_id: int, row_count: int = 0):
132151
from baserow.contrib.database.table.handler import TableUsageHandler

backend/src/baserow/contrib/database/views/signals.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def update_view_index_if_view_group_by_changes(sender, view_group_by, **kwargs):
8080
def view_loaded_create_indexes_and_columns(sender, view, table_model, **kwargs):
8181
from baserow.contrib.database.table.tasks import (
8282
setup_created_by_and_last_modified_by_column,
83+
setup_m2m_field_indexes_if_not_exist,
8384
)
8485
from baserow.contrib.database.views.handler import ViewIndexingHandler
8586

@@ -88,6 +89,8 @@ def view_loaded_create_indexes_and_columns(sender, view, table_model, **kwargs):
8889
table = view.table
8990
if not table.last_modified_by_column_added or not table.created_by_column_added:
9091
setup_created_by_and_last_modified_by_column.delay(table_id=view.table.id)
92+
if not table.missing_m2m_indexes_added:
93+
setup_m2m_field_indexes_if_not_exist.delay(table_id=view.table_id)
9194

9295

9396
@receiver(field_signals.fields_type_changed)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Ensure m2m field indexes are all set.",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2025-12-19"
9+
}

0 commit comments

Comments
 (0)