Skip to content

Commit 28d39d8

Browse files
ikreymertw4l
andauthored
Fix migration to avoid duplicate collection slugs and names (#2318)
Follow-up to #2301 Updates the 0039 migration to ensure collection slugs and names are unique by: - Removing all indexes - Setting `slug` to random value - Adding unique index to `slug` field. - Attempting to set slug from name using `slug_from_name()` - If rejected due to duplicate, append `-<counter>` at end of slug. Also update name with ` <counter>`. - Now that names should also be unique, add unique index on name field. --------- Co-authored-by: Tessa Walsh <[email protected]>
1 parent 6797b41 commit 28d39d8

File tree

2 files changed

+58
-6
lines changed

2 files changed

+58
-6
lines changed

backend/btrixcloud/migrations/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import os
6+
import traceback
67
from pymongo.errors import OperationFailure
78

89

@@ -73,6 +74,7 @@ async def run(self):
7374
await self.set_db_version()
7475
except OperationFailure as err:
7576
print(f"Error running migration {self.migration_version}: {err}")
77+
traceback.print_exc()
7678
return False
7779

7880
else:

backend/btrixcloud/migrations/migration_0039_coll_slugs.py

+56-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
Migration 0039 -- collection slugs
33
"""
44

5+
from uuid import UUID
6+
7+
from pymongo.errors import DuplicateKeyError
8+
from pymongo.collation import Collation
9+
import pymongo
10+
511
from btrixcloud.migrations import BaseMigration
612
from btrixcloud.utils import slug_from_name
713

8-
914
MIGRATION_VERSION = "0039"
1015

1116

@@ -16,23 +21,68 @@ class Migration(BaseMigration):
1621
def __init__(self, mdb, **kwargs):
1722
super().__init__(mdb, migration_version=MIGRATION_VERSION)
1823

24+
async def dedup_slug(
25+
self, name: str, slug_base: str, coll_id: UUID, colls_mdb
26+
) -> None:
27+
"""attempt to set slug, if duplicate, append suffix until a valid slug is found
28+
also update original name with same suffix"""
29+
slug = slug_base
30+
count = 1
31+
32+
while True:
33+
try:
34+
await colls_mdb.find_one_and_update(
35+
{"_id": coll_id},
36+
{"$set": {"slug": slug}},
37+
)
38+
break
39+
except DuplicateKeyError:
40+
# pylint: disable=raise-missing-from
41+
count += 1
42+
slug = f"{slug_base}-{count}"
43+
44+
if count > 1:
45+
print(f"Duplicate collection name '{name}' set to '{name} {count}'")
46+
await colls_mdb.find_one_and_update(
47+
{"_id": coll_id}, {"$set": {"name": f"{name} {count}"}}
48+
)
49+
1950
async def migrate_up(self):
2051
"""Perform migration up.
2152
2253
Add slug to collections that don't have one yet, based on name
2354
"""
2455
colls_mdb = self.mdb["collections"]
56+
case_insensitive_collation = Collation(locale="en", strength=1)
57+
58+
await colls_mdb.drop_indexes()
59+
60+
# set slug to random value to ensure uniqueness
61+
await colls_mdb.update_many(
62+
{}, [{"$set": {"slug": {"$toString": {"$rand": {}}}}}]
63+
)
64+
65+
await colls_mdb.create_index(
66+
[("oid", pymongo.ASCENDING), ("slug", pymongo.ASCENDING)],
67+
unique=True,
68+
collation=case_insensitive_collation,
69+
)
2570

26-
async for coll_raw in colls_mdb.find({"slug": None}):
71+
async for coll_raw in colls_mdb.find({}):
2772
coll_id = coll_raw["_id"]
2873
try:
29-
await colls_mdb.find_one_and_update(
30-
{"_id": coll_id},
31-
{"$set": {"slug": slug_from_name(coll_raw.get("name", ""))}},
32-
)
74+
name = coll_raw.get("name", "")
75+
slug = slug_from_name(name)
76+
await self.dedup_slug(name, slug, coll_id, colls_mdb)
3377
# pylint: disable=broad-exception-caught
3478
except Exception as err:
3579
print(
3680
f"Error saving slug for collection {coll_id}: {err}",
3781
flush=True,
3882
)
83+
84+
await colls_mdb.create_index(
85+
[("oid", pymongo.ASCENDING), ("name", pymongo.ASCENDING)],
86+
unique=True,
87+
collation=case_insensitive_collation,
88+
)

0 commit comments

Comments
 (0)