py.lib.aw_db_tools
2024-02-27
Child:fd7d3b38860e
py.lib.aw_db_tools/src/aw_db_tools/migrator.py
..init
| awgur@0 | 1 # coding: utf-8 |
| awgur@0 | 2 """\ |
| awgur@0 | 3 Инструмент миграции схемы БД. |
| awgur@0 | 4 |
| awgur@0 | 5 """ |
| awgur@0 | 6 |
| awgur@0 | 7 from os.path import exists, join as p_join, isdir |
| awgur@0 | 8 from os import listdir |
| awgur@0 | 9 |
| awgur@0 | 10 from . import Error |
| awgur@0 | 11 |
| awgur@0 | 12 |
| awgur@0 | 13 class MigrateError(Error): |
| awgur@0 | 14 """\ |
| awgur@0 | 15 Общий класс ошибок миграции |
| awgur@0 | 16 """ |
| awgur@0 | 17 |
| awgur@0 | 18 |
| awgur@0 | 19 class MigrateManager(object): |
| awgur@0 | 20 """\ |
| awgur@0 | 21 Менеджер миграции |
| awgur@0 | 22 """ |
| awgur@0 | 23 def __init__(self, control_table: str, migrate_env: str): |
| awgur@0 | 24 """ |
| awgur@0 | 25 :param control_table: Имя таблицы, хранящей метаданные миграции |
| awgur@0 | 26 :param migrate_env: Директория, хранящая SQL-скрипты миграции |
| awgur@0 | 27 """ |
| awgur@0 | 28 self.control_table = control_table |
| awgur@0 | 29 |
| awgur@0 | 30 if not exists(migrate_env): |
| awgur@0 | 31 raise MigrateError('Migrate enviroment not found') |
| awgur@0 | 32 |
| awgur@0 | 33 self.schema = p_join(migrate_env, 'schema.sql') |
| awgur@0 | 34 if not exists(self.schema): |
| awgur@0 | 35 raise MigrateError(f'Schema file not found: {self.schema}') |
| awgur@0 | 36 |
| awgur@0 | 37 self.patch_dir = p_join(migrate_env, 'patch') |
| awgur@0 | 38 if not isdir(self.patch_dir): |
| awgur@0 | 39 raise MigrateError(f'Patch dir not found or not directory: {self.patch_dir}') |
| awgur@0 | 40 |
| awgur@0 | 41 def get_patch_files(self, ver: int): |
| awgur@0 | 42 """\ |
| awgur@0 | 43 Получение из директории файлов миграции списка применяемых к данному экземпляру БД |
| awgur@0 | 44 """ |
| awgur@0 | 45 res = {} |
| awgur@0 | 46 for f in listdir(self.patch_dir): |
| awgur@0 | 47 if not f.lower().endswith('.sql'): |
| awgur@0 | 48 continue |
| awgur@0 | 49 |
| awgur@0 | 50 _f = f.strip().split('.') |
| awgur@0 | 51 |
| awgur@0 | 52 try: |
| awgur@0 | 53 _ver = int(_f[0]) |
| awgur@0 | 54 |
| awgur@0 | 55 except (TypeError, ValueError) as e: |
| awgur@0 | 56 raise MigrateError(f'Error on parse version "{_f[0]}" of file "{f}": {e}') |
| awgur@0 | 57 |
| awgur@0 | 58 except IndexError: |
| awgur@0 | 59 raise MigrateError(f'Error on get version from filename: {f}') |
| awgur@0 | 60 |
| awgur@0 | 61 if _ver in res: |
| awgur@0 | 62 raise MigrateError(f'Version duplicates on parse file: {f}') |
| awgur@0 | 63 |
| awgur@0 | 64 res[_ver] = p_join(self.patch_dir, f) |
| awgur@0 | 65 |
| awgur@0 | 66 for i in sorted(res.keys()): |
| awgur@0 | 67 if i > ver: |
| awgur@0 | 68 yield i, res[i] |
| awgur@0 | 69 |
| awgur@0 | 70 @staticmethod |
| awgur@0 | 71 def get_commands(file: str): |
| awgur@0 | 72 """\ |
| awgur@0 | 73 Получение из файлов серий команд, которые необходимо применять на БД |
| awgur@0 | 74 """ |
| awgur@0 | 75 buf = [] |
| awgur@0 | 76 with open(file) as IN: |
| awgur@0 | 77 for l in IN: |
| awgur@0 | 78 if l.lstrip().startswith('--'): |
| awgur@0 | 79 if buf: |
| awgur@0 | 80 yield '\n'.join(buf) |
| awgur@0 | 81 buf[:] = [] |
| awgur@0 | 82 |
| awgur@0 | 83 else: |
| awgur@0 | 84 buf.append(l) |
| awgur@0 | 85 |
| awgur@0 | 86 if buf: |
| awgur@0 | 87 yield '\n'.join(buf) |
| awgur@0 | 88 |
| awgur@0 | 89 def init_db(self, db): |
| awgur@0 | 90 """\ |
| awgur@0 | 91 Инициализация БД. |
| awgur@0 | 92 |
| awgur@0 | 93 :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python |
| awgur@0 | 94 """ |
| awgur@0 | 95 cursor = db.cursor() |
| awgur@0 | 96 for c in self.get_commands(self.schema): |
| awgur@0 | 97 cursor.execute(c) |
| awgur@0 | 98 db.commit() |
| awgur@0 | 99 |
| awgur@0 | 100 db.commit() |
| awgur@0 | 101 |
| awgur@0 | 102 def check(self, db): |
| awgur@0 | 103 """\ |
| awgur@0 | 104 Проверка БД на соответствие. |
| awgur@0 | 105 |
| awgur@0 | 106 :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python |
| awgur@0 | 107 """ |
| awgur@0 | 108 cursor = db.cursor() |
| awgur@0 | 109 cursor.execute(f"SELECT version FROM {self.control_table}") |
| awgur@0 | 110 q = cursor.fetchone() |
| awgur@0 | 111 del cursor |
| awgur@0 | 112 |
| awgur@0 | 113 if q is None: |
| awgur@0 | 114 ver = -1 |
| awgur@0 | 115 else: |
| awgur@0 | 116 ver = int(q[0]) |
| awgur@0 | 117 |
| awgur@0 | 118 new_ver = ver |
| awgur@0 | 119 cursor = db.cursor() |
| awgur@0 | 120 for up_ver, patch_file in self.get_patch_files(ver): |
| awgur@0 | 121 new_ver = up_ver |
| awgur@0 | 122 for cmd in self.get_commands(patch_file): |
| awgur@0 | 123 cursor.execute(cmd) |
| awgur@0 | 124 db.commit() |
| awgur@0 | 125 |
| awgur@0 | 126 cursor.execute(f"DELETE FROM {self.control_table}") |
| awgur@0 | 127 |
| awgur@0 | 128 cursor.execute(f""" |
| awgur@0 | 129 INSERT INTO {self.control_table} (version) |
| awgur@0 | 130 VALUES ({new_ver}) |
| awgur@0 | 131 """) |
| awgur@0 | 132 db.commit() |
| awgur@0 | 133 |
| awgur@0 | 134 @staticmethod |
| awgur@0 | 135 def get_conn_from_my_obj(obj: object): |
| awgur@0 | 136 """\ |
| awgur@0 | 137 Получиение объекта соединения из обёрток, которые я сам себе пишу для работы с DB-API |
| awgur@0 | 138 |
| awgur@0 | 139 :param obj: Получение объекта-подключения из объектов БД своего стиля оформления. |
| awgur@0 | 140 :return: |
| awgur@0 | 141 """ |
| awgur@0 | 142 if hasattr(obj, '_conn'): |
| awgur@0 | 143 return obj._conn |
| awgur@0 | 144 elif hasattr(obj, '_con'): |
| awgur@0 | 145 return obj._con |
| awgur@0 | 146 else: |
| awgur@0 | 147 raise TypeError('No known connection object in given database object found') |