|
|
@@ -2,6 +2,13 @@ from django.db import migrations
|
|
|
from django.apps import apps
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
|
+def disable_fk_checks(cursor):
|
|
|
+ cursor.execute("PRAGMA foreign_keys = OFF;")
|
|
|
+
|
|
|
+def enable_fk_checks(cursor):
|
|
|
+ cursor.execute("PRAGMA foreign_keys = ON;")
|
|
|
+
|
|
|
+
|
|
|
def copy_tags_raw_sql(cursor, old_table, old_id_column, new_item_id):
|
|
|
# Get content_type_id for ScrobblableItem
|
|
|
ct_id = ContentType.objects.get(app_label='scrobbles', model='scrobblableitem').id
|
|
|
@@ -35,7 +42,7 @@ def get_table_columns(cursor, table_name, vendor):
|
|
|
raise RuntimeError(f"Unsupported DB vendor: {vendor}")
|
|
|
|
|
|
|
|
|
-def create_and_copy_data(apps, schema_editor, lookup_keys):
|
|
|
+def create_and_copy_data(apps, schema_editor, lookup_keys, old_to_new_mappings):
|
|
|
app_label, model_name, model_id_field = lookup_keys
|
|
|
|
|
|
db = schema_editor.connection
|
|
|
@@ -45,7 +52,6 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
|
|
|
|
|
|
Model = apps.get_model(app_label, model_name)
|
|
|
ScrobblableItem = apps.get_model("scrobbles", "ScrobblableItem")
|
|
|
- ObjectWithGenres = apps.get_model("scrobbles", "ObjectWithGenres")
|
|
|
|
|
|
table = Model._meta.db_table
|
|
|
scrobble_table = "scrobbles_scrobble"
|
|
|
@@ -71,6 +77,9 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
|
|
|
cursor.execute(f"SELECT {select_clause} FROM {table}")
|
|
|
rows = cursor.fetchall()
|
|
|
|
|
|
+ mapping = {}
|
|
|
+ old_to_new_mappings[Model] = mapping
|
|
|
+
|
|
|
# -----------------------
|
|
|
# 2. Process rows
|
|
|
# -----------------------
|
|
|
@@ -88,6 +97,8 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
|
|
|
base.modified = data.get("modified")
|
|
|
base.save(update_fields=["created", "modified"])
|
|
|
|
|
|
+ mapping[old_id] = base.id
|
|
|
+
|
|
|
# -----------------------
|
|
|
# 3. Update media table FK
|
|
|
# -----------------------
|
|
|
@@ -121,6 +132,43 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
|
|
|
|
|
|
print(f" Finished migrating {app_label}.{model_name}")
|
|
|
|
|
|
+def reassign_foreign_keys(apps, schema_editor, old_to_new_mappings):
|
|
|
+ """
|
|
|
+ Detect all FKs pointing to old media tables and reassign to new ScrobblableItem.
|
|
|
+ """
|
|
|
+ print(old_to_new_mappings)
|
|
|
+ db = schema_editor.connection
|
|
|
+ cursor = db.cursor()
|
|
|
+
|
|
|
+ disable_fk_checks(cursor)
|
|
|
+ for model in apps.get_models():
|
|
|
+ table = model._meta.db_table
|
|
|
+ for field in model._meta.get_fields():
|
|
|
+ # Skip genre field
|
|
|
+ if getattr(field, 'name', None) in ['genre', 'tags']:
|
|
|
+ continue
|
|
|
+
|
|
|
+ if field.is_relation and field.many_to_one and field.related_model in old_to_new_mappings:
|
|
|
+ mapping = old_to_new_mappings[field.related_model]
|
|
|
+ fk_column = field.column
|
|
|
+ print(f"Updating FK {model._meta.label}.{field.name} ({table}.{fk_column})…")
|
|
|
+
|
|
|
+ for old_id, new_id in mapping.items():
|
|
|
+ cursor.execute(f"UPDATE {table} SET {fk_column} = %s WHERE {fk_column} = %s",
|
|
|
+ [new_id, old_id])
|
|
|
+
|
|
|
+ # ManyToMany FKs
|
|
|
+ if field.many_to_many and not field.auto_created:
|
|
|
+ through_table = field.remote_field.through._meta.db_table
|
|
|
+ from_col = field.m2m_column_name()
|
|
|
+ to_model = field.remote_field.model
|
|
|
+ if to_model in old_to_new_mappings:
|
|
|
+ mapping = old_to_new_mappings[to_model]
|
|
|
+ print(f"Updating M2M {through_table}.{from_col} → ScrobblableItem")
|
|
|
+ for old_id, new_id in mapping.items():
|
|
|
+ cursor.execute(f"UPDATE {through_table} SET {field.m2m_reverse_name()} = %s WHERE {field.m2m_reverse_name()} = %s",
|
|
|
+ [new_id, old_id])
|
|
|
+ enable_fk_checks(cursor)
|
|
|
|
|
|
def backfill_media(apps, schema_editor):
|
|
|
models = (
|
|
|
@@ -142,9 +190,12 @@ def backfill_media(apps, schema_editor):
|
|
|
('webpages', 'WebPage', 'web_page_id'),
|
|
|
)
|
|
|
|
|
|
+ old_to_new_mappings = {}
|
|
|
for keys in models:
|
|
|
- create_and_copy_data(apps, schema_editor, keys)
|
|
|
+ create_and_copy_data(apps, schema_editor, keys, old_to_new_mappings)
|
|
|
|
|
|
+ # After all media tables migrated, reassign FKs and M2Ms
|
|
|
+ reassign_foreign_keys(apps, schema_editor, old_to_new_mappings)
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
dependencies = [
|
|
|
@@ -169,5 +220,85 @@ class Migration(migrations.Migration):
|
|
|
]
|
|
|
|
|
|
operations = [
|
|
|
- migrations.RunPython(backfill_media, reverse_code=migrations.RunPython.noop)
|
|
|
+ migrations.RunPython(backfill_media, reverse_code=migrations.RunPython.noop),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="beer",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="board_game",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="book",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="brick_set",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="food",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="geo_location",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="life_event",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="long_play_complete",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="long_play_seconds",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="mood",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="paper",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="podcast_episode",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="puzzle",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="sport_event",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="task",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="track",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="trail",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="video",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="video_game",
|
|
|
+ ),
|
|
|
+ migrations.RemoveField(
|
|
|
+ model_name="scrobble",
|
|
|
+ name="web_page",
|
|
|
+ ),
|
|
|
]
|