# Convert HQ sites.site_id from bigint to farm-matching slug (varchar). from django.db import migrations, models import django.db.models.deletion def _unique_slug(candidate: str, used: set[str]) -> str: final = candidate[:64] n = 0 while final in used or not final: n += 1 suffix = f"-{n}" final = f"{candidate[: 64 - len(suffix)]}{suffix}" used.add(final) return final def forwards_convert_site_pk(apps, schema_editor): from django.utils.text import slugify connection = schema_editor.connection Site = apps.get_model("farms", "Site") Kandang = apps.get_model("farms", "Kandang") ActiveSite = apps.get_model("sync", "ActiveSite") sites = list( Site.objects.all().values( "site_id", "site_name", "user_id", "active_site_id", "created_at", "updated_at", ) ) kandangs = list( Kandang.objects.all().values( "kandang_id", "kandang_name", "feed_in_button_urls", "site_id", "created_at", "updated_at", ) ) active_source = { row.pk: str(row.source_site_id).strip() for row in ActiveSite.objects.all() if str(row.source_site_id or "").strip() } used: set[str] = set() mapping: dict[int, str] = {} for row in sites: old = row["site_id"] candidate = active_source.get(row["active_site_id"]) or "" if not candidate: base = slugify(row["site_name"] or "")[:40] or "site" candidate = f"{base}-legacy-{old}" mapping[old] = _unique_slug(candidate, used) with connection.cursor() as cursor: if connection.vendor == "sqlite": cursor.execute("PRAGMA foreign_keys=OFF") cursor.execute( """ CREATE TABLE sites_new ( site_id varchar(64) NOT NULL PRIMARY KEY, site_name varchar(30) NOT NULL, user_id bigint NOT NULL REFERENCES user_access (user_id) DEFERRABLE INITIALLY DEFERRED, active_site_id bigint UNIQUE REFERENCES active_sites (active_site_id) DEFERRABLE INITIALLY DEFERRED, created_at datetime NOT NULL, updated_at datetime NOT NULL ) """ ) for row in sites: cursor.execute( """ INSERT INTO sites_new (site_id, site_name, user_id, active_site_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) """, [ mapping[row["site_id"]], row["site_name"], row["user_id"], row["active_site_id"], row["created_at"], row["updated_at"], ], ) cursor.execute("DROP TABLE sites") cursor.execute("ALTER TABLE sites_new RENAME TO sites") cursor.execute( "CREATE INDEX IF NOT EXISTS idx_sites_user_id ON sites (user_id)" ) cursor.execute( """ CREATE TABLE kandang_new ( kandang_id integer NOT NULL PRIMARY KEY AUTOINCREMENT, kandang_name varchar(30) NOT NULL, feed_in_button_urls text NOT NULL DEFAULT '[]', site_id varchar(64) NOT NULL REFERENCES sites (site_id) DEFERRABLE INITIALLY DEFERRED, created_at datetime NOT NULL, updated_at datetime NOT NULL ) """ ) import json for row in kandangs: urls = row["feed_in_button_urls"] if not isinstance(urls, str): urls = json.dumps(urls if urls is not None else []) cursor.execute( """ INSERT INTO kandang_new (kandang_id, kandang_name, feed_in_button_urls, site_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) """, [ row["kandang_id"], row["kandang_name"], urls, mapping[row["site_id"]], row["created_at"], row["updated_at"], ], ) cursor.execute("DROP TABLE kandang") cursor.execute("ALTER TABLE kandang_new RENAME TO kandang") cursor.execute( "CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)" ) cursor.execute("PRAGMA foreign_keys=ON") elif connection.vendor == "postgresql": cursor.execute( "ALTER TABLE sites ADD COLUMN IF NOT EXISTS site_id_new varchar(64)" ) for old, new in mapping.items(): cursor.execute( "UPDATE sites SET site_id_new = %s WHERE site_id = %s", [new, old], ) cursor.execute( "ALTER TABLE kandang ADD COLUMN IF NOT EXISTS site_id_new varchar(64)" ) cursor.execute( """ UPDATE kandang AS k SET site_id_new = s.site_id_new FROM sites AS s WHERE k.site_id = s.site_id """ ) cursor.execute( """ ALTER TABLE kandang DROP CONSTRAINT IF EXISTS kandang_site_id_fkey """ ) # Django default name variants cursor.execute( """ DO $$ DECLARE r RECORD; BEGIN FOR r IN SELECT conname FROM pg_constraint WHERE conrelid = 'kandang'::regclass AND contype = 'f' AND pg_get_constraintdef(oid) ILIKE '%%site_id%%' LOOP EXECUTE format('ALTER TABLE kandang DROP CONSTRAINT %I', r.conname); END LOOP; END $$; """ ) cursor.execute("ALTER TABLE kandang DROP COLUMN site_id") cursor.execute( "ALTER TABLE kandang RENAME COLUMN site_id_new TO site_id" ) cursor.execute("ALTER TABLE sites DROP CONSTRAINT sites_pkey") cursor.execute("ALTER TABLE sites DROP COLUMN site_id") cursor.execute("ALTER TABLE sites RENAME COLUMN site_id_new TO site_id") cursor.execute("ALTER TABLE sites ALTER COLUMN site_id SET NOT NULL") cursor.execute("ALTER TABLE sites ADD PRIMARY KEY (site_id)") cursor.execute("ALTER TABLE kandang ALTER COLUMN site_id SET NOT NULL") cursor.execute( """ ALTER TABLE kandang ADD CONSTRAINT kandang_site_id_fkey FOREIGN KEY (site_id) REFERENCES sites (site_id) ON DELETE CASCADE """ ) cursor.execute( "CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)" ) else: raise NotImplementedError( f"sites.site_id slug migration unsupported for {connection.vendor}" ) class Migration(migrations.Migration): dependencies = [ ("farms", "0018_rename_city_site_to_active_site"), ("sync", "0007_uniq_source_site_id"), ] operations = [ migrations.SeparateDatabaseAndState( database_operations=[ migrations.RunPython(forwards_convert_site_pk, migrations.RunPython.noop), ], state_operations=[ migrations.AlterField( model_name="site", name="site_id", field=models.SlugField(max_length=64, primary_key=True, serialize=False), ), migrations.AlterField( model_name="kandang", name="site", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="kandangs", to="farms.site", ), ), ], ), ]