Scope device uniqueness per parent, null type/location for sub-components, hide sub-components from list, flat battery summary for parent devices
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sbin/migrate_parent_key.py
|
||||
==========================
|
||||
Adds parent_key column, changes uniqueness from UNIQUE(name) to
|
||||
UNIQUE(parent_key, name), and clears device_type/location for sub-components.
|
||||
|
||||
SQLite: recreates the device table (required to change constraints).
|
||||
MariaDB: uses ALTER TABLE. Check the index name with:
|
||||
SHOW INDEX FROM device WHERE Column_name = 'name';
|
||||
|
||||
Usage:
|
||||
python sbin/migrate_parent_key.py # SQLite (batteries.db)
|
||||
MARIADB_URL='mysql+...' python sbin/migrate_parent_key.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def snapshot(src: Path) -> None:
|
||||
dst = src.parent / f"{src.name}.{date.today().isoformat()}.snapshot"
|
||||
shutil.copy2(src, dst)
|
||||
print(f"Snapshot: {dst.name}")
|
||||
|
||||
|
||||
def migrate_sqlite() -> None:
|
||||
import sqlite3
|
||||
|
||||
db_path = REPO_ROOT / "batteries.db"
|
||||
if not db_path.exists():
|
||||
print("batteries.db not found — nothing to migrate.")
|
||||
return
|
||||
|
||||
snapshot(db_path)
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.execute("PRAGMA foreign_keys = OFF")
|
||||
|
||||
# Check if parent_key already exists (idempotency)
|
||||
cols = {row[1] for row in conn.execute("PRAGMA table_info(device)")}
|
||||
if "parent_key" in cols:
|
||||
print("parent_key already present — running data-only fixup.")
|
||||
conn.execute("UPDATE device SET parent_key = COALESCE(parent_id, -1)")
|
||||
conn.execute("UPDATE device SET device_type = NULL, location = NULL WHERE parent_id IS NOT NULL")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Done.")
|
||||
return
|
||||
|
||||
conn.execute("ALTER TABLE device RENAME TO device_old")
|
||||
|
||||
conn.execute("""
|
||||
CREATE TABLE device (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
battery_slots INTEGER NOT NULL DEFAULT 1,
|
||||
device_type VARCHAR(50),
|
||||
battery_size VARCHAR(20),
|
||||
location VARCHAR(100),
|
||||
notes TEXT,
|
||||
ha_entity_id VARCHAR(100),
|
||||
parent_id INTEGER REFERENCES device(id) ON DELETE SET NULL,
|
||||
parent_key INTEGER NOT NULL DEFAULT -1,
|
||||
CONSTRAINT uq_device_parent_key_name UNIQUE (parent_key, name)
|
||||
)
|
||||
""")
|
||||
|
||||
conn.execute("""
|
||||
INSERT INTO device
|
||||
(id, name, battery_slots, device_type, battery_size, location,
|
||||
notes, ha_entity_id, parent_id, parent_key)
|
||||
SELECT
|
||||
id, name, battery_slots,
|
||||
CASE WHEN parent_id IS NOT NULL THEN NULL ELSE device_type END,
|
||||
battery_size,
|
||||
CASE WHEN parent_id IS NOT NULL THEN NULL ELSE location END,
|
||||
notes, ha_entity_id, parent_id,
|
||||
COALESCE(parent_id, -1)
|
||||
FROM device_old
|
||||
""")
|
||||
|
||||
conn.execute("DROP TABLE device_old")
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.commit()
|
||||
|
||||
count = conn.execute("SELECT COUNT(*) FROM device").fetchone()[0]
|
||||
print(f"SQLite migration complete — {count} device(s) migrated.")
|
||||
conn.close()
|
||||
|
||||
|
||||
def migrate_mariadb(url: str) -> None:
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
engine = create_engine(url)
|
||||
with engine.connect() as conn:
|
||||
# Add column (skip if already present)
|
||||
try:
|
||||
conn.execute(text(
|
||||
"ALTER TABLE device ADD COLUMN parent_key INT NOT NULL DEFAULT -1"
|
||||
))
|
||||
except Exception:
|
||||
print("parent_key column already exists — skipping ADD COLUMN.")
|
||||
|
||||
conn.execute(text("UPDATE device SET parent_key = COALESCE(parent_id, -1)"))
|
||||
conn.execute(text(
|
||||
"UPDATE device SET device_type = NULL, location = NULL WHERE parent_id IS NOT NULL"
|
||||
))
|
||||
|
||||
# Drop old UNIQUE index on name — check actual name first:
|
||||
# SHOW INDEX FROM device WHERE Column_name = 'name';
|
||||
# Common names: 'name' or 'ix_device_name'
|
||||
for idx_name in ("name", "ix_device_name"):
|
||||
try:
|
||||
conn.execute(text(f"ALTER TABLE device DROP INDEX `{idx_name}`"))
|
||||
print(f"Dropped index '{idx_name}'.")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
conn.execute(text(
|
||||
"ALTER TABLE device ADD CONSTRAINT uq_device_parent_key_name "
|
||||
"UNIQUE (parent_key, name)"
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
print("MariaDB migration complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mariadb_url = os.environ.get("MARIADB_URL", "").strip()
|
||||
if mariadb_url:
|
||||
migrate_mariadb(mariadb_url)
|
||||
else:
|
||||
migrate_sqlite()
|
||||
Reference in New Issue
Block a user