Files
MIND/backend/internals/db_migration.py
2025-08-30 23:12:50 +02:00

398 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
from functools import lru_cache
from typing import Dict, Type
from backend.base.definitions import Constants, DBMigrator
from backend.base.logging import LOGGER
from backend.internals.db import get_db, iter_commit
@lru_cache(1)
def get_db_migration_map() -> Dict[int, Type[DBMigrator]]:
"""Get a map of the database version to the migrator class for that version
to one database version higher. E.g. 2 -> Migrate2To3.
Returns:
Dict[int, Type[DBMigrator]]: The map.
"""
return {
m.start_version: m
for m in DBMigrator.__subclasses__()
}
@lru_cache(1)
def get_latest_db_version() -> int:
"""Get the latest database version supported.
Returns:
int: The version.
"""
return max(get_db_migration_map()) + 1
def migrate_db() -> None:
"""
Migrate a MIND database from it's current version
to the newest version supported by the MIND version installed.
"""
from backend.internals.settings import Settings
s = Settings()
current_db_version = s.sv.database_version
newest_version = get_latest_db_version()
if current_db_version == newest_version:
get_db_migration_map.cache_clear()
return
LOGGER.info("Migrating database to newer version...")
LOGGER.debug(
"Database migration: %d -> %d",
current_db_version, newest_version
)
db_migration_map = get_db_migration_map()
for start_version in iter_commit(range(current_db_version, newest_version)):
if start_version not in db_migration_map:
continue
db_migration_map[start_version]().run()
s.update({"database_version": start_version + 1})
get_db().execute("VACUUM;")
s.clear_cache()
get_db_migration_map.cache_clear()
return
class MigrateToUTC(DBMigrator):
start_version = 1
def run(self) -> None:
# V1 -> V2
from datetime import datetime
from time import time
cursor = get_db()
t = time()
utc_offset = datetime.fromtimestamp(t) - datetime.utcfromtimestamp(t)
cursor.execute("SELECT time, id FROM reminders;")
new_reminders = [
[
round((
datetime.fromtimestamp(r["time"]) - utc_offset
).timestamp()),
r["id"]
]
for r in cursor
]
cursor.executemany(
"UPDATE reminders SET time = ? WHERE id = ?;",
new_reminders
)
return
class MigrateAddColor(DBMigrator):
start_version = 2
def run(self) -> None:
# V2 -> V3
get_db().executescript("""
ALTER TABLE reminders
ADD color VARCHAR(7);
ALTER TABLE templates
ADD color VARCHAR(7);
""")
return
class MigrateFixRQ(DBMigrator):
start_version = 3
def run(self) -> None:
# V3 -> V4
get_db().executescript("""
UPDATE reminders
SET repeat_quantity = repeat_quantity || 's'
WHERE repeat_quantity NOT LIKE '%s';
""")
return
class MigrateToReminderServices(DBMigrator):
start_version = 4
def run(self) -> None:
# V4 -> V5
get_db().executescript("""
BEGIN TRANSACTION;
PRAGMA defer_foreign_keys = ON;
CREATE TEMPORARY TABLE temp_reminder_services(
reminder_id,
static_reminder_id,
template_id,
notification_service_id
);
-- Reminders
INSERT INTO temp_reminder_services(
reminder_id, notification_service_id
)
SELECT id, notification_service
FROM reminders;
CREATE TEMPORARY TABLE temp_reminders AS
SELECT
id, user_id, title, text,
time, repeat_quantity, repeat_interval, original_time,
color
FROM reminders;
DROP TABLE reminders;
CREATE TABLE reminders(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
time INTEGER NOT NULL,
repeat_quantity VARCHAR(15),
repeat_interval INTEGER,
original_time INTEGER,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO reminders
SELECT * FROM temp_reminders;
-- Templates
INSERT INTO temp_reminder_services(
template_id, notification_service_id
)
SELECT id, notification_service
FROM templates;
CREATE TEMPORARY TABLE temp_templates AS
SELECT id, user_id, title, text, color
FROM templates;
DROP TABLE templates;
CREATE TABLE templates(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
color VARCHAR(7),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO templates
SELECT * FROM temp_templates;
INSERT INTO reminder_services
SELECT * FROM temp_reminder_services;
COMMIT;
""")
return
class MigrateRemoveUser1(DBMigrator):
start_version = 5
def run(self) -> None:
# V5 -> V6
from backend.base.custom_exceptions import (AccessUnauthorized,
UserNotFound)
from backend.implementations.users import Users
try:
Users().login('User1', 'Password1').delete()
except (UserNotFound, AccessUnauthorized):
pass
return
class MigrateAddWeekdays(DBMigrator):
start_version = 6
def run(self) -> None:
# V6 -> V7
get_db().executescript("""
ALTER TABLE reminders
ADD weekdays VARCHAR(13);
""")
return
class MigrateAddAdmin(DBMigrator):
start_version = 7
def run(self) -> None:
# V7 -> V8
from backend.implementations.users import Users
from backend.internals.settings import Settings
cursor = get_db()
cursor.executescript("""
DROP TABLE config;
CREATE TABLE IF NOT EXISTS config(
key VARCHAR(255) PRIMARY KEY,
value BLOB NOT NULL
);
"""
)
Settings()._insert_missing_settings()
cursor.executescript("""
ALTER TABLE users
ADD admin BOOL NOT NULL DEFAULT 0;
"""
)
users = Users()
if 'admin' in users:
users.get_one(
users.user_db.username_to_id('admin')
).update_username('admin_old')
users.add(
Constants.ADMIN_USERNAME, Constants.ADMIN_PASSWORD,
force=True,
is_admin=True
)
return
class MigrateHostSettingsToDB(DBMigrator):
start_version = 8
def run(self) -> None:
# V8 -> V9
# In newer versions, the variables don't exist anymore, and behaviour
# was to then set the values to the default values. But that's already
# taken care of by the settings, so nothing to do here anymore.
return
class MigrateUpdateManifest(DBMigrator):
start_version = 9
def run(self) -> None:
# V9 -> V10
# There used to be a migration here that fixed the manifest file.
# That has since been replaced by the dynamic endpoint serving the JSON.
# So the migration doesn't do anything anymore, and a function used
# doesn't exist anymore, so the whole migration is just removed.
return
class MigrateAddEnabled(DBMigrator):
start_version = 10
def run(self) -> None:
# V10 -> V11
get_db().execute("""
ALTER TABLE reminders
ADD enabled BOOL NOT NULL DEFAULT 1;
""")
return
class MigrateSetDBBackupFolder(DBMigrator):
start_version = 11
def run(self) -> None:
# V11 -> V12
from backend.internals.settings import Settings, SettingsValues
s = Settings()
sv = s.get_settings()
if sv.db_backup_folder == '':
s.update({"db_backup_folder": SettingsValues.db_backup_folder})
return
class MigrateAddCronScheduleColumn(DBMigrator):
start_version = 12
def run(self) -> None:
# V12 -> V13
get_db().executescript("""
PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION;
CREATE TEMPORARY TABLE temp_reminders_13 AS
SELECT * FROM reminders;
DROP TABLE reminders;
CREATE TABLE IF NOT EXISTS reminders(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
text TEXT,
time INTEGER NOT NULL,
repeat_quantity VARCHAR(15),
repeat_interval INTEGER,
original_time INTEGER,
weekdays VARCHAR(13),
cron_schedule VARCHAR(255),
color VARCHAR(7),
enabled BOOL NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO reminders
SELECT
id, user_id,
title, text, time,
repeat_quantity, repeat_interval,
original_time, weekdays,
NULL AS cron_schedule,
color, enabled
FROM temp_reminders_13;
COMMIT;
PRAGMA foreign_keys = ON;
""")
class MigrateAddMFAColumn(DBMigrator):
start_version = 13
def run(self) -> None:
# V13 -> V14
get_db().executescript("""
ALTER TABLE users
ADD mfa_apprise_url TEXT;
""")
return