py.lib.aw_db_tools
2024-02-27
Child:fd7d3b38860e
py.lib.aw_db_tools/src/aw_db_tools/migrator.py
..init
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/aw_db_tools/migrator.py Tue Feb 27 21:06:11 2024 +0300 1.3 @@ -0,0 +1,147 @@ 1.4 +# coding: utf-8 1.5 +"""\ 1.6 +Инструмент миграции схемы БД. 1.7 + 1.8 +""" 1.9 + 1.10 +from os.path import exists, join as p_join, isdir 1.11 +from os import listdir 1.12 + 1.13 +from . import Error 1.14 + 1.15 + 1.16 +class MigrateError(Error): 1.17 + """\ 1.18 + Общий класс ошибок миграции 1.19 + """ 1.20 + 1.21 + 1.22 +class MigrateManager(object): 1.23 + """\ 1.24 + Менеджер миграции 1.25 + """ 1.26 + def __init__(self, control_table: str, migrate_env: str): 1.27 + """ 1.28 + :param control_table: Имя таблицы, хранящей метаданные миграции 1.29 + :param migrate_env: Директория, хранящая SQL-скрипты миграции 1.30 + """ 1.31 + self.control_table = control_table 1.32 + 1.33 + if not exists(migrate_env): 1.34 + raise MigrateError('Migrate enviroment not found') 1.35 + 1.36 + self.schema = p_join(migrate_env, 'schema.sql') 1.37 + if not exists(self.schema): 1.38 + raise MigrateError(f'Schema file not found: {self.schema}') 1.39 + 1.40 + self.patch_dir = p_join(migrate_env, 'patch') 1.41 + if not isdir(self.patch_dir): 1.42 + raise MigrateError(f'Patch dir not found or not directory: {self.patch_dir}') 1.43 + 1.44 + def get_patch_files(self, ver: int): 1.45 + """\ 1.46 + Получение из директории файлов миграции списка применяемых к данному экземпляру БД 1.47 + """ 1.48 + res = {} 1.49 + for f in listdir(self.patch_dir): 1.50 + if not f.lower().endswith('.sql'): 1.51 + continue 1.52 + 1.53 + _f = f.strip().split('.') 1.54 + 1.55 + try: 1.56 + _ver = int(_f[0]) 1.57 + 1.58 + except (TypeError, ValueError) as e: 1.59 + raise MigrateError(f'Error on parse version "{_f[0]}" of file "{f}": {e}') 1.60 + 1.61 + except IndexError: 1.62 + raise MigrateError(f'Error on get version from filename: {f}') 1.63 + 1.64 + if _ver in res: 1.65 + raise MigrateError(f'Version duplicates on parse file: {f}') 1.66 + 1.67 + res[_ver] = p_join(self.patch_dir, f) 1.68 + 1.69 + for i in sorted(res.keys()): 1.70 + if i > ver: 1.71 + yield i, res[i] 1.72 + 1.73 + @staticmethod 1.74 + def get_commands(file: str): 1.75 + """\ 1.76 + Получение из файлов серий команд, которые необходимо применять на БД 1.77 + """ 1.78 + buf = [] 1.79 + with open(file) as IN: 1.80 + for l in IN: 1.81 + if l.lstrip().startswith('--'): 1.82 + if buf: 1.83 + yield '\n'.join(buf) 1.84 + buf[:] = [] 1.85 + 1.86 + else: 1.87 + buf.append(l) 1.88 + 1.89 + if buf: 1.90 + yield '\n'.join(buf) 1.91 + 1.92 + def init_db(self, db): 1.93 + """\ 1.94 + Инициализация БД. 1.95 + 1.96 + :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python 1.97 + """ 1.98 + cursor = db.cursor() 1.99 + for c in self.get_commands(self.schema): 1.100 + cursor.execute(c) 1.101 + db.commit() 1.102 + 1.103 + db.commit() 1.104 + 1.105 + def check(self, db): 1.106 + """\ 1.107 + Проверка БД на соответствие. 1.108 + 1.109 + :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python 1.110 + """ 1.111 + cursor = db.cursor() 1.112 + cursor.execute(f"SELECT version FROM {self.control_table}") 1.113 + q = cursor.fetchone() 1.114 + del cursor 1.115 + 1.116 + if q is None: 1.117 + ver = -1 1.118 + else: 1.119 + ver = int(q[0]) 1.120 + 1.121 + new_ver = ver 1.122 + cursor = db.cursor() 1.123 + for up_ver, patch_file in self.get_patch_files(ver): 1.124 + new_ver = up_ver 1.125 + for cmd in self.get_commands(patch_file): 1.126 + cursor.execute(cmd) 1.127 + db.commit() 1.128 + 1.129 + cursor.execute(f"DELETE FROM {self.control_table}") 1.130 + 1.131 + cursor.execute(f""" 1.132 + INSERT INTO {self.control_table} (version) 1.133 + VALUES ({new_ver}) 1.134 + """) 1.135 + db.commit() 1.136 + 1.137 + @staticmethod 1.138 + def get_conn_from_my_obj(obj: object): 1.139 + """\ 1.140 + Получиение объекта соединения из обёрток, которые я сам себе пишу для работы с DB-API 1.141 + 1.142 + :param obj: Получение объекта-подключения из объектов БД своего стиля оформления. 1.143 + :return: 1.144 + """ 1.145 + if hasattr(obj, '_conn'): 1.146 + return obj._conn 1.147 + elif hasattr(obj, '_con'): 1.148 + return obj._con 1.149 + else: 1.150 + raise TypeError('No known connection object in given database object found')