Files
dashboard-cpsp-executive/backend/apps/farms/migrations/0019_site_id_slug.py
T

239 lines
8.6 KiB
Python

# 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",
),
),
],
),
]