py.lib.aw_db_tools

Yohn Y. 2024-02-27 Child:fd7d3b38860e

0:4b0d10bfa023 Go to Latest

py.lib.aw_db_tools/src/aw_db_tools/migrator.py

..init

History
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')