mirror of
https://github.com/EDeev/y.calendarkin.git
synced 2026-10-08 00:39:27 +03:00
code/sql.py на psycopg 3 с пулом соединений (переподключается сам); таблицы users, urls, alarms — в PostgreSQL user и url зарезервированы. Внутренние номера пользователей сохраняются: на них ссылаются файлы календарей data/icals/<номер>.ics. scripts/migrate_sqlite.py переносит старые базы со сверкой (проверено на копии рабочих: 25 пользователей, 9 подписок). Тесты хранилища и переноса — на PostgreSQL в CI; compose поднимает бота вместе с базой.
60 lines
3 KiB
Python
60 lines
3 KiB
Python
"""Перенос данных Я.Календаркина из SQLite (users.db, clock.db) в PostgreSQL.
|
||
|
||
python scripts/migrate_sqlite.py --sqlite-dir /path/to/db --dsn postgresql://…
|
||
|
||
Внутренние номера пользователей сохраняются: на них ссылаются файлы календарей data/icals/<номер>.ics.
|
||
"""
|
||
import argparse
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
|
||
import psycopg
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "code"))
|
||
from sql import SCHEMA # noqa: E402
|
||
|
||
|
||
def migrate(sqlite_dir, dsn, force=False):
|
||
users_db = sqlite3.connect(os.path.join(sqlite_dir, "users.db"))
|
||
clock_db = sqlite3.connect(os.path.join(sqlite_dir, "clock.db"))
|
||
|
||
users = users_db.execute("SELECT id, user_id FROM user ORDER BY id").fetchall()
|
||
urls = users_db.execute("SELECT user_id, status, url_ical, time_zone FROM url").fetchall()
|
||
alarms = clock_db.execute("SELECT user_id, daily, start, alarm_1, alarm_2, status_2 FROM alarm").fetchall()
|
||
known = {u[0] for u in users}
|
||
|
||
with psycopg.connect(dsn) as conn:
|
||
conn.execute(SCHEMA)
|
||
if conn.execute("SELECT count(*) FROM users").fetchone()[0]:
|
||
if not force:
|
||
raise SystemExit("В PostgreSQL уже есть данные — перенос остановлен (--force очистит таблицы)")
|
||
conn.execute("TRUNCATE users, urls, alarms RESTART IDENTITY CASCADE")
|
||
with conn.cursor() as cur:
|
||
cur.executemany("INSERT INTO users (id, tg_id) VALUES (%s, %s) ON CONFLICT DO NOTHING", users)
|
||
cur.executemany("INSERT INTO urls (user_id, status, url_ical, time_zone) VALUES (%s, %s, %s, %s) "
|
||
"ON CONFLICT DO NOTHING",
|
||
[(u, bool(s), url, tz) for u, s, url, tz in urls if u in known])
|
||
cur.executemany("INSERT INTO alarms (user_id, daily, start, alarm_1, alarm_2, status_2) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING",
|
||
[(u, bool(d), bool(s), a1, a2, bool(s2)) for u, d, s, a1, a2, s2 in alarms if u in known])
|
||
cur.execute("SELECT setval(pg_get_serial_sequence('users', 'id'), GREATEST((SELECT max(id) FROM users), 1))")
|
||
report = {name: (src, conn.execute(f"SELECT count(*) FROM {name}").fetchone()[0])
|
||
for name, src in (("users", len(users)), ("urls", len(urls)), ("alarms", len(alarms)))}
|
||
|
||
for name, (src, dst) in report.items():
|
||
print(f"{name:7} SQLite {src:>5} PostgreSQL {dst:>5} {'ok' if src == dst else 'MISMATCH'}")
|
||
return all(a == b for a, b in report.values())
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||
parser.add_argument("--sqlite-dir", required=True)
|
||
parser.add_argument("--dsn", default=os.getenv("DATABASE_URL"))
|
||
parser.add_argument("--force", action="store_true")
|
||
args = parser.parse_args()
|
||
sys.exit(0 if migrate(args.sqlite_dir, args.dsn, args.force) else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|