commit 7a13ff3b7402b0cf3190b238b78770b319ccceaf Author: brusnitsyn Date: Thu Apr 16 17:57:58 2026 +0900 first commit diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c6efcb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.env +.venv +__pycache__ +*.pyc +logs +.git +.gitignore +.codex diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b0fe51b --- /dev/null +++ b/.env.example @@ -0,0 +1,28 @@ +MSSQL_CONNECTION_STRING=mssql+pymssql://user:password@host:1433/database +MSSQL_CHARSET=cp1251 +MSSQL_CONNECT_TIMEOUT=30 +MSSQL_POOL_RECYCLE=1800 +MSSQL_TABLE_RETRIES=2 +MSSQL_POOL_SIZE=1 +MSSQL_MAX_OVERFLOW=0 + +POSTGRES_CONNECTION_STRING=postgresql://user:password@host:5432/database + +EMAIL_HOST=smtp.example.org +EMAIL_PORT=465 +EMAIL_USER=user@example.org +EMAIL_PASSWORD=password +EMAIL_FROM=user@example.org +EMAIL_TO=admin@example.org +EMAIL_SUBJECT=Результат миграции данных MSSQL → PostgreSQL + +REPLICATOR_SCHEMA=replicator +ENABLE_TIMESCALE=false +DRY_RUN=false +READ_LIMIT=0 +CHUNK_SIZE=5000 +WRITE_CHUNK_SIZE=5000 +CREATE_FOREIGN_KEYS=true +QUEUE_POLL_SECONDS=1 +SCHEDULE_GRACE_SECONDS=60 +TZ=Asia/Yakutsk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba8bfab --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +__pycache__/ +*.py[cod] +logs/*.log +logs/*.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..92e11d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.12-alpine AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN apk add --no-cache \ + build-base \ + freetds \ + freetds-dev \ + linux-headers \ + postgresql-dev \ + python3-dev \ + tzdata + +COPY req.txt . +RUN pip install --upgrade pip \ + && pip wheel --wheel-dir /wheels -r req.txt + +FROM python:3.12-alpine AS runtime + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN apk add --no-cache \ + freetds \ + libpq \ + tzdata + +COPY req.txt . +COPY --from=builder /wheels /wheels +RUN pip install --upgrade pip \ + && pip install --no-index --find-links=/wheels -r req.txt \ + && rm -rf /wheels + +COPY app ./app +COPY main.py . +COPY sql ./sql + +RUN mkdir -p logs + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..18b665e --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""Application package.""" diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..6554d70 --- /dev/null +++ b/app/api.py @@ -0,0 +1,161 @@ +from datetime import datetime +from typing import List, Optional + +try: + from fastapi import FastAPI, HTTPException + from pydantic import BaseModel +except ImportError: + FastAPI = None + HTTPException = None + BaseModel = object + +from .config import Config +from .queue import migration_queue +from .table_config_repository import TableConfigRepository + + +# ============================================================================ +# FASTAPI +# ============================================================================ +class MigrationRequest(BaseModel): + """Запрос на запуск миграции из API.""" + tables: Optional[List[str]] = None + send_email: bool = True + dry_run: Optional[bool] = None + read_limit: Optional[int] = None + force_full: bool = False + run_at: Optional[datetime] = None + delay_seconds: Optional[int] = None + + +class ScheduleRequest(BaseModel): + """Запрос на создание расписания миграции.""" + schedule_type: str + tables: Optional[List[str]] = None + send_email: bool = True + dry_run: Optional[bool] = None + read_limit: Optional[int] = None + interval_seconds: Optional[int] = None + daily_time: Optional[str] = None + start_at: Optional[datetime] = None + name: Optional[str] = None + enabled: bool = True + catch_up_missed_runs: bool = False + initial_force_full: bool = False + + +def create_app(): + """Создание FastAPI приложения.""" + if FastAPI is None: + return None + + api = FastAPI(title="Syncio Migration API", version="0.1.0") + + @api.on_event("startup") + def startup_queue(): + if Config.START_API_WORKER: + migration_queue.start() + + @api.get("/health") + def health(): + return {"status": "ok"} + + @api.get("/tables") + def tables(): + from sqlalchemy import create_engine + + config = Config() + engine = create_engine(config.POSTGRES_CONNECTION_STRING) + repository = TableConfigRepository(config, engine) + table_configs = repository.load_configs(seed_defaults=True) + return [ + { + "source_table": table.source_table, + "target_table": table.pg_table, + "mode": table.mode, + "life_table": table.life_table, + "datetime_column": table.datetime_column, + "sequence_column": table.sequence_column, + "operation_column": table.operation_column, + "initial_load_mode": table.initial_load_mode, + "order_columns": table.incremental_order_columns, + "timescale": table.timescale, + "enabled": table.enabled, + } + for table in table_configs + ] + + @api.get("/migrations/status") + def migration_status(): + return migration_queue.get_status() + + @api.get("/migrations/queue") + def migration_queue_list(): + return migration_queue.list_jobs() + + @api.get("/migrations/queue/{job_id}") + def migration_queue_job(job_id: str): + job = migration_queue.get_job(job_id) + if not job: + raise HTTPException(status_code=404, detail="Job not found") + return job + + @api.get("/migrations/schedules") + def migration_schedule_list(): + return migration_queue.list_schedules() + + @api.get("/migrations/schedules/{schedule_id}") + def migration_schedule(schedule_id: str): + schedule = migration_queue.get_schedule(schedule_id) + if not schedule: + raise HTTPException(status_code=404, detail="Schedule not found") + return schedule + + @api.post("/migrations/run") + def run_migration(request: MigrationRequest): + try: + job = migration_queue.enqueue( + tables=request.tables, + send_email=request.send_email, + dry_run=request.dry_run, + read_limit=request.read_limit, + force_full=request.force_full, + run_at=request.run_at, + delay_seconds=request.delay_seconds, + ) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) + + return {"status": "queued", "job": job} + + @api.post("/migrations/tables/{table_name}/run") + def run_table_migration(table_name: str): + job = migration_queue.enqueue(tables=[table_name], send_email=True) + return {"status": "queued", "job": job} + + @api.post("/migrations/schedules") + def create_schedule(request: ScheduleRequest): + try: + schedule = migration_queue.create_schedule( + schedule_type=request.schedule_type, + tables=request.tables, + send_email=request.send_email, + dry_run=request.dry_run, + read_limit=request.read_limit, + interval_seconds=request.interval_seconds, + daily_time=request.daily_time, + start_at=request.start_at, + name=request.name, + enabled=request.enabled, + catch_up_missed_runs=request.catch_up_missed_runs, + initial_force_full=request.initial_force_full, + ) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) + + return {"status": "scheduled", "schedule": schedule} + + return api + + +app = create_app() diff --git a/app/backfill_watermarks.py b/app/backfill_watermarks.py new file mode 100644 index 0000000..b2383ea --- /dev/null +++ b/app/backfill_watermarks.py @@ -0,0 +1,71 @@ +import argparse +import json +import traceback + +from .config import Config + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Backfill migration_state watermark-ами из MSSQL Life_ таблиц." + ) + parser.add_argument( + "--tables", + nargs="+", + help="Список source_table/target_table для обработки. По умолчанию берутся все incremental-таблицы.", + ) + parser.add_argument( + "--overwrite", + action="store_true", + help="Перезаписать уже существующие watermark в replicator.migration_state.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Только показать, какие watermark будут записаны, без сохранения в PostgreSQL.", + ) + parser.add_argument( + "--json", + action="store_true", + help="Вывести итог в JSON.", + ) + return parser.parse_args() + + +def main(): + try: + from .migrator import DatabaseMigrator + + args = parse_args() + config = Config() + migrator = DatabaseMigrator(config) + summary = migrator.backfill_watermarks( + table_names=args.tables, + overwrite=args.overwrite, + dry_run=args.dry_run, + ) + + if args.json: + print(json.dumps(summary, ensure_ascii=False, indent=2, default=str)) + else: + print("\n" + "=" * 60) + print("BACKFILL WATERMARKS ЗАВЕРШЕН") + print("=" * 60) + print(f"Обработано: {summary['processed']}") + print(f"Обновлено: {summary['updated']}") + print(f"Пропущено: {summary['skipped']}") + print(f"Ошибок: {summary['failed']}") + print("=" * 60) + + return 1 if summary['failed'] > 0 else 0 + except KeyboardInterrupt: + print("\nBackfill прерван пользователем") + return 130 + except Exception as e: + print(f"Критическая ошибка: {e}") + print(traceback.format_exc()) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/app/cli.py b/app/cli.py new file mode 100644 index 0000000..b54e7b6 --- /dev/null +++ b/app/cli.py @@ -0,0 +1,45 @@ +import traceback + +from .config import Config + + +def main(): + """Основная функция""" + try: + from .migrator import DatabaseMigrator + + # Инициализация конфигурации + config = Config() + + # Создание мигратора + migrator = DatabaseMigrator(config) + + # Запуск миграции + report = migrator.run_migration() + + # Очистка старых логов + migrator.cleanup_old_logs(days_to_keep=7) + + # Вывод краткой статистики в консоль + print("\n" + "="*60) + print("МИГРАЦИЯ ЗАВЕРШЕНА!") + print("="*60) + print(f"Успешно: {report['summary']['successful_tables']}/{report['summary']['total_tables']} таблиц") + print(f"Всего строк: {report['summary']['total_rows']}") + print(f"Продолжительность: {report['summary']['duration']}") + print(f"Лог-файл: {migrator.logger.log_file}") + print("="*60) + + # Возвращаем код завершения + if report['summary']['failed_tables'] > 0: + return 1 # Есть ошибки + else: + return 0 # Успех + + except KeyboardInterrupt: + print("\nМиграция прервана пользователем") + return 130 + except Exception as e: + print(f"Критическая ошибка: {e}") + print(traceback.format_exc()) + return 1 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..fd0adf6 --- /dev/null +++ b/app/config.py @@ -0,0 +1,156 @@ +import os +import logging +from dataclasses import dataclass, field +from typing import List, Optional + +try: + from dotenv import load_dotenv +except ImportError: + def load_dotenv(path: str = '.env'): + """Минимальный fallback для локального .env, если python-dotenv еще не установлен.""" + if not os.path.exists(path): + return False + with open(path, encoding='utf-8') as env_file: + for raw_line in env_file: + line = raw_line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + key, value = line.split('=', 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + os.environ.setdefault(key, value) + return True + + +load_dotenv() + + +@dataclass +class TableMigrationConfig: + """Настройки миграции одной таблицы.""" + source_table: str + target_table: Optional[str] = None + mode: str = "full" + initial_load_mode: str = "full_then_incremental" + life_table: Optional[str] = None + datetime_column: str = "x_DateTime" + sequence_column: Optional[str] = None + order_columns: List[str] = field(default_factory=list) + operation_column: Optional[str] = None + delete_operations: List[str] = field(default_factory=lambda: ['d']) + upsert_operations: List[str] = field(default_factory=lambda: ['i', 'u']) + primary_key: List[str] = field(default_factory=list) + exclude_columns: List[str] = field(default_factory=list) + timescale: bool = False + timescale_time_column: Optional[str] = None + enabled: bool = True + + @property + def pg_table(self) -> str: + return (self.target_table or self.source_table).lower() + + @property + def read_table(self) -> str: + return self.life_table or self.source_table + + @property + def incremental_order_columns(self) -> List[str]: + if self.order_columns: + return self.order_columns + columns = [self.datetime_column] + if self.sequence_column: + columns.append(self.sequence_column) + return columns + +# ============================================================================ +# КОНФИГУРАЦИЯ +# ============================================================================ +class Config: + """Конфигурация приложения""" + # Настройки MSSQL + MSSQL_CONNECTION_STRING = os.getenv('MSSQL_CONNECTION_STRING') + MSSQL_CHARSET = os.getenv('MSSQL_CHARSET', 'cp1251') + MSSQL_POOL_RECYCLE = int(os.getenv('MSSQL_POOL_RECYCLE', '1800')) + MSSQL_CONNECT_TIMEOUT = int(os.getenv('MSSQL_CONNECT_TIMEOUT', '30')) + MSSQL_TABLE_RETRIES = int(os.getenv('MSSQL_TABLE_RETRIES', '2')) + MSSQL_POOL_SIZE = int(os.getenv('MSSQL_POOL_SIZE', '1')) + MSSQL_MAX_OVERFLOW = int(os.getenv('MSSQL_MAX_OVERFLOW', '0')) + + # Настройки PostgreSQL + POSTGRES_CONNECTION_STRING = os.getenv('POSTGRES_CONNECTION_STRING') + + # Настройки email + EMAIL_HOST = os.getenv('EMAIL_HOST') + EMAIL_PORT = int(os.getenv('EMAIL_PORT', '465')) + EMAIL_USER = os.getenv('EMAIL_USER') + EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD') + EMAIL_FROM = os.getenv('EMAIL_FROM') + EMAIL_TO = [ + email.strip() + for email in os.getenv('EMAIL_TO', '').split(',') + if email.strip() + ] + EMAIL_SUBJECT = os.getenv('EMAIL_SUBJECT', 'Результат миграции данных MSSQL → PostgreSQL') + + # Настройки логирования + LOG_DIR = 'logs' + LOG_FILE = 'migration_log_{timestamp}.log' + LOG_LEVEL = logging.INFO + + # Настройки миграции + CHUNK_SIZE = int(os.getenv('CHUNK_SIZE', '5000')) + WRITE_CHUNK_SIZE = int(os.getenv('WRITE_CHUNK_SIZE', str(CHUNK_SIZE))) + BATCH_SIZE = 10 # Через сколько чанков выводить прогресс + REPLICATOR_SCHEMA = os.getenv('REPLICATOR_SCHEMA', 'replicator') + STATE_TABLE = 'migration_state' + TABLE_CONFIGS_TABLE = 'migration_tables' + ENABLE_TIMESCALE = os.getenv('ENABLE_TIMESCALE', 'false').lower() == 'true' + DRY_RUN = os.getenv('DRY_RUN', 'false').lower() == 'true' + READ_LIMIT = int(os.getenv('READ_LIMIT', '0')) or None + QUEUE_POLL_SECONDS = float(os.getenv('QUEUE_POLL_SECONDS', '1')) + SCHEDULE_GRACE_SECONDS = int(os.getenv('SCHEDULE_GRACE_SECONDS', '60')) + START_API_WORKER = os.getenv('START_API_WORKER', 'true').lower() == 'true' + CREATE_FOREIGN_KEYS = os.getenv('CREATE_FOREIGN_KEYS', 'true').lower() == 'true' + + # Настройки таблиц. Для инкрементальной миграции заполните life_table + # primary_key и exclude_columns, чтобы запись можно было делать идемпотентно. + DEFAULT_TABLE_MIGRATIONS = [ + TableMigrationConfig( + source_table='Oms_LPU', + target_table='Oms_LPU', + mode='incremental', + initial_load_mode='full_then_incremental', + life_table='Life_oms_LPU', + datetime_column='x_DateTime', + sequence_column='LPULifeID', + order_columns=['x_DateTime', 'LPULifeID'], + operation_column='x_Operation', + primary_key=['LPUID'], + exclude_columns=[ + 'LPULifeID', + 'x_Operation', + 'x_DateTime', + 'x_Seance', + 'x_User', + ], + timescale=False, + enabled=True, + ), + # TableMigrationConfig( + # source_table='stt_MigrationPatient', + # target_table='stt_MigrationPatient', + # mode='incremental', + # initial_load_mode='full_then_incremental', + # life_table='Life_stt_MigrationPatient', + # datetime_column='x_DateTime', + # sequence_column='MigrationPatientLifeID', + # order_columns=['x_DateTime', 'MigrationPatientLifeID'], + # operation_column='x_Operation', + # primary_key=['Id'], + # exclude_columns=['MigrationPatientLifeID', 'x_Operation', 'x_DateTime', 'x_Seance', 'x_User'], + # timescale=True, + # timescale_time_column='x_DateTime', + # ), + ] + TABLE_MIGRATIONS = list(DEFAULT_TABLE_MIGRATIONS) + TABLES_TO_COPY = [table.source_table for table in TABLE_MIGRATIONS] diff --git a/app/email_sender.py b/app/email_sender.py new file mode 100644 index 0000000..2419a0e --- /dev/null +++ b/app/email_sender.py @@ -0,0 +1,56 @@ +import os +import smtplib +from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import List + +from .config import Config + + +class EmailSender: + """Класс для отправки email уведомлений""" + + def __init__(self, config: Config): + self.config = config + + def send_email(self, subject: str, body: str, attachments: List[str] = None): + """Отправка email с вложениями""" + if not all([self.config.EMAIL_HOST, self.config.EMAIL_USER, + self.config.EMAIL_PASSWORD, self.config.EMAIL_FROM]): + print("Настройки email не заполнены. Отправка email пропущена.") + return False + + try: + # Создаем сообщение + msg = MIMEMultipart() + msg['From'] = self.config.EMAIL_FROM + msg['To'] = ', '.join(self.config.EMAIL_TO) + msg['Subject'] = subject + + # Добавляем текст сообщения + msg.attach(MIMEText(body, 'plain', 'utf-8')) + + # Добавляем вложения + if attachments: + for attachment_path in attachments: + if os.path.exists(attachment_path): + with open(attachment_path, 'rb') as f: + part = MIMEApplication(f.read(), Name=os.path.basename(attachment_path)) + part['Content-Disposition'] = f'attachment; filename="{os.path.basename(attachment_path)}"' + msg.attach(part) + + # Подключаемся к SMTP серверу + with smtplib.SMTP_SSL(self.config.EMAIL_HOST, self.config.EMAIL_PORT) as server: + server.login(self.config.EMAIL_USER, self.config.EMAIL_PASSWORD) + server.send_message(msg) + + print(f"Email успешно отправлен на {', '.join(self.config.EMAIL_TO)}") + return True + + except Exception as e: + print(f"Ошибка при отправке email: {e}") + return False + + +# ============================================================================ diff --git a/app/logging_utils.py b/app/logging_utils.py new file mode 100644 index 0000000..22a8570 --- /dev/null +++ b/app/logging_utils.py @@ -0,0 +1,147 @@ +import json +import logging +import os +import traceback +from datetime import datetime +from typing import Any, Dict, Optional + +from .config import Config + + +class MigrationLogger: + """Класс для логирования процесса миграции""" + + def __init__(self, config: Config): + self.config = config + self.start_time = datetime.now() + self.timestamp = self.start_time.strftime("%Y%m%d_%H%M%S") + self.log_file = os.path.join( + config.LOG_DIR, + config.LOG_FILE.format(timestamp=self.timestamp) + ) + self.stats = { + 'total_tables': len(config.TABLES_TO_COPY), + 'copied_tables': [], + 'failed_tables': [], + 'total_rows': 0, + 'start_time': self.start_time, + 'end_time': None, + 'errors': [] + } + + # Создаем директорию для логов + os.makedirs(config.LOG_DIR, exist_ok=True) + + # Настраиваем logging + self.setup_logging() + + def setup_logging(self): + """Настройка системы логирования""" + logging.basicConfig( + level=self.config.LOG_LEVEL, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(self.log_file, encoding='utf-8'), + logging.StreamHandler() # Вывод в консоль + ] + ) + self.logger = logging.getLogger(__name__) + + def log_info(self, message: str): + """Логирование информационного сообщения""" + self.logger.info(message) + + def log_error(self, message: str, exception: Optional[Exception] = None): + """Логирование ошибки""" + error_details = { + 'message': message, + 'exception': str(exception) if exception else None, + 'traceback': traceback.format_exc() if exception else None, + 'timestamp': datetime.now().isoformat() + } + self.stats['errors'].append(error_details) + self.logger.error(f"{message}. Ошибка: {exception}") + + def log_warning(self, message: str): + """Логирование предупреждения""" + self.logger.warning(message) + + def log_table_start(self, table_name: str): + """Логирование начала обработки таблицы""" + self.logger.info(f"{'='*60}") + self.logger.info(f"Начало обработки таблицы: {table_name}") + self.logger.info(f"{'='*60}") + + def log_table_success(self, table_name: str, row_count: int): + """Логирование успешного копирования таблицы""" + self.stats['copied_tables'].append({ + 'name': table_name, + 'row_count': row_count, + 'timestamp': datetime.now().isoformat() + }) + self.stats['total_rows'] += row_count + self.logger.info(f"Таблица {table_name} успешно скопирована ({row_count} строк)") + + def log_table_failure(self, table_name: str, error: str): + """Логирование ошибки при копировании таблицы""" + self.stats['failed_tables'].append({ + 'name': table_name, + 'error': error, + 'timestamp': datetime.now().isoformat() + }) + self.logger.error(f"Ошибка при копировании таблицы {table_name}: {error}") + + def log_progress(self, table_name: str, chunk_num: int, total_rows: int): + """Логирование прогресса загрузки""" + self.logger.info(f"Таблица {table_name}: загружено чанков {chunk_num}, строк {total_rows}") + + def generate_report(self) -> Dict[str, Any]: + """Генерация итогового отчета""" + self.stats['end_time'] = datetime.now() + duration = self.stats['end_time'] - self.stats['start_time'] + self.stats['duration_seconds'] = duration.total_seconds() + self.stats['duration_human'] = str(duration) + + report = { + 'summary': { + 'total_tables': self.stats['total_tables'], + 'successful_tables': len(self.stats['copied_tables']), + 'failed_tables': len(self.stats['failed_tables']), + 'success_rate': (len(self.stats['copied_tables']) / self.stats['total_tables'] * 100) + if self.stats['total_tables'] > 0 else 0, + 'total_rows': self.stats['total_rows'], + 'start_time': self.stats['start_time'].isoformat(), + 'end_time': self.stats['end_time'].isoformat(), + 'duration': self.stats['duration_human'] + }, + 'successful_tables': [ + {'name': t['name'], 'rows': t['row_count']} + for t in self.stats['copied_tables'] + ], + 'failed_tables': [ + {'name': t['name'], 'error': t['error']} + for t in self.stats['failed_tables'] + ], + 'errors': self.stats['errors'] + } + + # Сохраняем отчет в JSON + report_file = os.path.join( + self.config.LOG_DIR, + f"migration_report_{self.timestamp}.json" + ) + with open(report_file, 'w', encoding='utf-8') as f: + json.dump(report, f, ensure_ascii=False, indent=2, default=str) + + return report + + def get_log_content(self) -> str: + """Получение содержимого лог-файла""" + try: + with open(self.log_file, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + return f"Ошибка при чтении лог-файла: {e}" + + +# ============================================================================ diff --git a/app/migrator.py b/app/migrator.py new file mode 100644 index 0000000..d626f62 --- /dev/null +++ b/app/migrator.py @@ -0,0 +1,1538 @@ +import os +import re +import time +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +import pandas as pd +from sqlalchemy import create_engine, inspect, text +from sqlalchemy.exc import DBAPIError, OperationalError +from sqlalchemy.sql import sqltypes + +from .config import Config, TableMigrationConfig +from .email_sender import EmailSender +from .logging_utils import MigrationLogger +from .table_config_repository import TableConfigRepository + + +class DatabaseMigrator: + """Основной класс для миграции данных""" + + def __init__(self, config: Config): + self.config = config + + # Подключение к PostgreSQL и загрузка конфигурации таблиц из БД + self.dst_engine = create_engine(config.POSTGRES_CONNECTION_STRING) + self.table_config_repository = TableConfigRepository(config, self.dst_engine) + self.table_configs = self.table_config_repository.load_configs(seed_defaults=True) + self.config.TABLE_MIGRATIONS = self.table_configs + self.config.TABLES_TO_COPY = [table.source_table for table in self.table_configs] + + self.logger = MigrationLogger(config) + self.email_sender = EmailSender(config) + self._source_columns_cache: Dict[str, List[Dict[str, Any]]] = {} + self._mssql_indexes_cache: Dict[str, List[Dict[str, Any]]] = {} + self._mssql_pk_cache: Dict[str, Optional[List[str]]] = {} + self._mssql_fk_cache: Dict[str, Optional[List[Dict[str, str]]]] = {} + + # Подключение к БД + self.logger.log_info( + f"Подключение к БД МИС (MSSQL), charset={self.config.MSSQL_CHARSET}" + ) + self.src_engine = self.create_mssql_engine() + + self.logger.log_info("Подключение к PostgreSQL") + + def create_mssql_engine(self): + """Создание SQLAlchemy engine для MSSQL с явной кодировкой pymssql.""" + return create_engine( + self.config.MSSQL_CONNECTION_STRING, + connect_args={ + 'charset': self.config.MSSQL_CHARSET, + 'login_timeout': self.config.MSSQL_CONNECT_TIMEOUT, + 'timeout': self.config.MSSQL_CONNECT_TIMEOUT, + }, + pool_pre_ping=True, + pool_recycle=self.config.MSSQL_POOL_RECYCLE, + pool_size=self.config.MSSQL_POOL_SIZE, + max_overflow=self.config.MSSQL_MAX_OVERFLOW, + ) + + def reconnect_mssql_engine(self): + """Пересоздание MSSQL engine после сетевого/драйверного сбоя.""" + try: + self.src_engine.dispose() + except Exception: + pass + self._source_columns_cache.clear() + self._mssql_indexes_cache.clear() + self._mssql_pk_cache.clear() + self._mssql_fk_cache.clear() + self.src_engine = self.create_mssql_engine() + + def is_retryable_mssql_error(self, exception: Exception) -> bool: + """Определение временной ошибки MSSQL/pymssql, при которой есть смысл повторить таблицу.""" + if isinstance(exception, (OperationalError, DBAPIError)): + message = str(exception).lower() + retry_markers = ( + 'dbprocess is dead', + 'adaptive server connection failed', + 'server closed the connection unexpectedly', + 'connection reset', + 'connection refused', + 'communication link failure', + 'lost connection', + 'closed connection', + 'not enabled', + '08s01', + ) + return any(marker in message for marker in retry_markers) + return False + + def migrate_table_once(self, table_config: TableMigrationConfig, force_full: bool = False) -> bool: + """Один проход миграции таблицы без retry-обертки.""" + if force_full: + self.logger.log_info(f"Force full reload для таблицы {table_config.source_table}") + success = self.migrate_full_table(table_config) + if success and table_config.mode == 'incremental' and table_config.life_table: + upper_bound = self.get_incremental_upper_bound(table_config) + self.save_watermark( + table_config.pg_table, + upper_bound['last_x_datetime'], + upper_bound['last_sequence_value'], + 0, + 'success', + ) + self.logger.log_info( + f"После force full сохранен watermark для {table_config.pg_table}: " + f"{upper_bound}" + ) + return success + + if table_config.mode == 'incremental': + return self.migrate_incremental_table(table_config) + + return self.migrate_full_table(table_config) + + @staticmethod + def quote_identifier(identifier: str) -> str: + """Экранирование SQL identifier для PostgreSQL/MSSQL.""" + return '"' + identifier.replace('"', '""') + '"' + + @staticmethod + def quote_mssql_identifier(identifier: str) -> str: + """Экранирование SQL Server identifier.""" + return '[' + identifier.replace(']', ']]') + ']' + + def qualify_table_name(self, table_name: str, schema: Optional[str] = None) -> str: + """Полное имя таблицы PostgreSQL с указанием схемы.""" + if schema: + return f'{self.quote_identifier(schema)}.{self.quote_identifier(table_name)}' + return self.quote_identifier(table_name) + + def compile_pg_type(self, source_type) -> str: + """Преобразование SQLAlchemy/MSSQL типа в тип PostgreSQL.""" + type_name = source_type.__class__.__name__.lower() + + if 'uniqueidentifier' in type_name or 'uuid' in type_name: + return 'uuid' + if 'bit' in type_name or isinstance(source_type, sqltypes.Boolean): + return 'boolean' + if isinstance(source_type, sqltypes.BigInteger): + return 'bigint' + if isinstance(source_type, sqltypes.SmallInteger): + return 'smallint' + if isinstance(source_type, sqltypes.Integer): + return 'integer' + if isinstance(source_type, sqltypes.Numeric): + precision = getattr(source_type, 'precision', None) + scale = getattr(source_type, 'scale', None) + if precision is not None and scale is not None: + return f'numeric({precision}, {scale})' + if precision is not None: + return f'numeric({precision})' + return 'numeric' + if isinstance(source_type, (sqltypes.Float, sqltypes.REAL)): + return 'double precision' + if isinstance(source_type, sqltypes.DateTime): + return 'timestamp' + if isinstance(source_type, sqltypes.Date): + return 'date' + if isinstance(source_type, sqltypes.Time): + return 'time' + if isinstance(source_type, sqltypes.CHAR): + length = getattr(source_type, 'length', None) + return f'char({length})' if length else 'char' + if isinstance(source_type, sqltypes.String): + length = getattr(source_type, 'length', None) + return f'varchar({length})' if length else 'text' + if isinstance(source_type, (sqltypes.Text, sqltypes.UnicodeText)): + return 'text' + if isinstance(source_type, sqltypes.LargeBinary): + return 'bytea' + + return 'text' + + def get_source_table_columns(self, table_name: str) -> List[Dict[str, Any]]: + """Получение колонок исходной MSSQL таблицы.""" + cache_key = table_name.lower() + if cache_key not in self._source_columns_cache: + self._source_columns_cache[cache_key] = inspect(self.src_engine).get_columns(table_name) + return self._source_columns_cache[cache_key] + + def get_target_table_columns(self, table_name: str) -> List[Dict[str, Any]]: + """Получение колонок целевой PostgreSQL таблицы.""" + return inspect(self.dst_engine).get_columns(table_name) + + def sync_target_schema(self, source_table: str, target_table: str): + """Добавление новых колонок из MSSQL в PostgreSQL как nullable.""" + if not self.table_exists(target_table): + return + + source_columns = self.get_source_table_columns(source_table) + target_columns = { + column['name'].lower(): column + for column in self.get_target_table_columns(target_table) + } + + for source_column in source_columns: + column_name = source_column['name'] + lower_name = column_name.lower() + if lower_name in target_columns: + target_type = str(target_columns[lower_name]['type']).lower() + source_type = self.compile_pg_type(source_column['type']).lower() + if target_type != source_type: + self.logger.log_warning( + f"Тип колонки {target_table}.{column_name} отличается: " + f"source={source_type}, target={target_type}. Автоизменение типа не выполняется" + ) + continue + + pg_type = self.compile_pg_type(source_column['type']) + self.logger.log_info( + f"Обнаружена новая колонка {source_table}.{column_name}; " + f"добавление в {target_table} как nullable ({pg_type})" + ) + + if self.config.DRY_RUN: + self.logger.log_info( + f"DRY RUN: пропущено добавление колонки {target_table}.{column_name}" + ) + continue + + sql = text( + f'ALTER TABLE {self.qualify_table_name(target_table)} ' + f'ADD COLUMN IF NOT EXISTS {self.quote_identifier(column_name)} {pg_type} NULL' + ) + with self.dst_engine.connect() as conn: + conn.execute(sql) + conn.commit() + + def get_table_config(self, table_name: str) -> TableMigrationConfig: + """Получение настроек таблицы по имени источника или цели.""" + table_config = self.table_config_repository.get_config(table_name) + return table_config or TableMigrationConfig(source_table=table_name) + + def table_exists(self, table_name: str) -> bool: + """Проверка существования таблицы в целевой БД.""" + return self.table_exists_in_schema(table_name, None) + + def table_exists_in_schema(self, table_name: str, schema: Optional[str]) -> bool: + """Проверка существования таблицы в указанной схеме PostgreSQL.""" + query = text(""" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = COALESCE(:schema, current_schema()) + AND table_name = :table_name + ) + """) + with self.dst_engine.connect() as conn: + return bool(conn.execute(query, {'schema': schema, 'table_name': table_name}).scalar()) + + def ensure_state_table(self): + """Создание таблицы состояния инкрементальной миграции.""" + qualified_state_table = self.qualify_table_name( + self.config.STATE_TABLE, + self.config.REPLICATOR_SCHEMA, + ) + sql = f""" + CREATE TABLE IF NOT EXISTS {qualified_state_table} ( + table_name text PRIMARY KEY, + last_x_datetime timestamp NULL, + last_sequence_value bigint NULL, + last_run_at timestamp NOT NULL DEFAULT now(), + rows_copied bigint NOT NULL DEFAULT 0, + status text NOT NULL DEFAULT 'pending', + error text NULL + ) + """ + with self.dst_engine.connect() as conn: + conn.execute(text( + f'CREATE SCHEMA IF NOT EXISTS {self.quote_identifier(self.config.REPLICATOR_SCHEMA)}' + )) + conn.execute(text(sql)) + conn.execute(text(f""" + ALTER TABLE {qualified_state_table} + ADD COLUMN IF NOT EXISTS last_sequence_value bigint NULL + """)) + conn.execute(text(f""" + DELETE FROM {qualified_state_table} AS duplicate_rows + USING {qualified_state_table} AS preserved_rows + WHERE duplicate_rows.table_name = preserved_rows.table_name + AND duplicate_rows.ctid < preserved_rows.ctid + """)) + conn.execute(text(f""" + CREATE UNIQUE INDEX IF NOT EXISTS idx_{self.config.STATE_TABLE}_table_name + ON {qualified_state_table} (table_name) + """)) + conn.commit() + + def get_last_watermark(self, table_name: str) -> Dict[str, Any]: + """Чтение последнего успешно обработанного x_DateTime.""" + empty_watermark = {'last_x_datetime': None, 'last_sequence_value': None} + if self.config.DRY_RUN and not self.table_exists_in_schema( + self.config.STATE_TABLE, + self.config.REPLICATOR_SCHEMA, + ): + return empty_watermark + + self.ensure_state_table() + qualified_state_table = self.qualify_table_name( + self.config.STATE_TABLE, + self.config.REPLICATOR_SCHEMA, + ) + sql = text(f""" + SELECT last_x_datetime, last_sequence_value + FROM {qualified_state_table} + WHERE table_name = :table_name + """) + with self.dst_engine.connect() as conn: + row = conn.execute(sql, {'table_name': table_name}).mappings().first() + + if not row: + return empty_watermark + + return { + 'last_x_datetime': row['last_x_datetime'], + 'last_sequence_value': row['last_sequence_value'], + } + + def save_watermark( + self, + table_name: str, + last_x_datetime: Optional[datetime], + last_sequence_value: Optional[int], + rows_copied: int, + status: str, + error: Optional[str] = None, + ): + """Сохранение состояния инкрементальной миграции.""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: состояние миграции {table_name} не обновляется") + return + + self.ensure_state_table() + qualified_state_table = self.qualify_table_name( + self.config.STATE_TABLE, + self.config.REPLICATOR_SCHEMA, + ) + sql = text(f""" + INSERT INTO {qualified_state_table} + (table_name, last_x_datetime, last_sequence_value, last_run_at, rows_copied, status, error) + VALUES + (:table_name, :last_x_datetime, :last_sequence_value, now(), :rows_copied, :status, :error) + ON CONFLICT (table_name) DO UPDATE SET + last_x_datetime = EXCLUDED.last_x_datetime, + last_sequence_value = EXCLUDED.last_sequence_value, + last_run_at = EXCLUDED.last_run_at, + rows_copied = EXCLUDED.rows_copied, + status = EXCLUDED.status, + error = EXCLUDED.error + """) + with self.dst_engine.connect() as conn: + conn.execute(sql, { + 'table_name': table_name, + 'last_x_datetime': last_x_datetime, + 'last_sequence_value': last_sequence_value, + 'rows_copied': rows_copied, + 'status': status, + 'error': error, + }) + conn.commit() + + def get_incremental_upper_bound(self, table_config: TableMigrationConfig) -> Dict[str, Any]: + """Фиксация верхней границы Life_ на начало запуска.""" + select_columns = [ + f"{self.quote_mssql_identifier(table_config.datetime_column)} AS max_datetime", + ] + if table_config.sequence_column: + select_columns.append( + f"{self.quote_mssql_identifier(table_config.sequence_column)} AS max_sequence_value" + ) + else: + select_columns.append("CAST(NULL AS bigint) AS max_sequence_value") + + order_columns = ', '.join([ + f"{self.quote_mssql_identifier(column)} DESC" + for column in table_config.incremental_order_columns + ]) + sql = text(f""" + SELECT TOP 1 {', '.join(select_columns)} + FROM {self.quote_mssql_identifier(table_config.read_table)} + ORDER BY {order_columns} + """) + with self.src_engine.connect() as conn: + row = conn.execute(sql).mappings().first() + + if not row: + return {'last_x_datetime': None, 'last_sequence_value': None} + + return { + 'last_x_datetime': row['max_datetime'], + 'last_sequence_value': row['max_sequence_value'], + } + + def read_incremental_chunks( + self, + table_config: TableMigrationConfig, + last_watermark: Dict[str, Any], + upper_bound: Dict[str, Any], + read_limit: Optional[int] = None, + ): + """Чтение Life_ таблицы по x_DateTime чанками.""" + where_parts = [ + f"{self.quote_mssql_identifier(table_config.datetime_column)} <= :upper_bound_datetime", + ] + params = {'upper_bound_datetime': upper_bound['last_x_datetime']} + + if table_config.sequence_column and upper_bound.get('last_sequence_value') is not None: + where_parts.append(f""" + ( + {self.quote_mssql_identifier(table_config.datetime_column)} < :upper_bound_datetime + OR ( + {self.quote_mssql_identifier(table_config.datetime_column)} = :upper_bound_datetime + AND {self.quote_mssql_identifier(table_config.sequence_column)} <= :upper_bound_sequence + ) + ) + """) + params['upper_bound_sequence'] = upper_bound['last_sequence_value'] + + if last_watermark.get('last_x_datetime') is not None: + if table_config.sequence_column and last_watermark.get('last_sequence_value') is not None: + where_parts.append(f""" + ( + {self.quote_mssql_identifier(table_config.datetime_column)} > :last_watermark_datetime + OR ( + {self.quote_mssql_identifier(table_config.datetime_column)} = :last_watermark_datetime + AND {self.quote_mssql_identifier(table_config.sequence_column)} > :last_watermark_sequence + ) + ) + """) + params['last_watermark_sequence'] = last_watermark['last_sequence_value'] + else: + where_parts.append( + f"{self.quote_mssql_identifier(table_config.datetime_column)} > :last_watermark_datetime" + ) + params['last_watermark_datetime'] = last_watermark['last_x_datetime'] + + order_columns = ', '.join([ + self.quote_mssql_identifier(column) + for column in table_config.incremental_order_columns + ]) + top_clause = f"TOP {int(read_limit)} " if read_limit else "" + + sql = text(f""" + SELECT {top_clause}* + FROM {self.quote_mssql_identifier(table_config.read_table)} + WHERE {' AND '.join(where_parts)} + ORDER BY {order_columns} + """) + + return pd.read_sql_query( + sql, + self.src_engine, + params=params, + chunksize=self.config.CHUNK_SIZE, + ) + + def read_full_chunks( + self, + table_name: str, + read_limit: Optional[int] = None, + ): + """Чтение полной таблицы чанками с опциональным лимитом для проверки.""" + if read_limit: + sql = text(f"SELECT TOP {int(read_limit)} * FROM {self.quote_mssql_identifier(table_name)}") + return pd.read_sql_query(sql, self.src_engine, chunksize=self.config.CHUNK_SIZE) + + return pd.read_sql_table(table_name, self.src_engine, chunksize=self.config.CHUNK_SIZE) + + def write_dataframe_batch( + self, + chunk: pd.DataFrame, + table_name: str, + if_exists: str = 'append', + ): + """Batch-запись DataFrame в PostgreSQL.""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущена запись {len(chunk)} строк в {table_name}") + return + + chunk.to_sql( + table_name, + self.dst_engine, + if_exists=if_exists, + index=False, + chunksize=self.config.WRITE_CHUNK_SIZE, + method='multi', + ) + + def prepare_incremental_chunk( + self, + chunk: pd.DataFrame, + table_config: TableMigrationConfig, + ) -> pd.DataFrame: + """Удаление служебных Life_ полей перед записью в целевую таблицу.""" + exclude_columns = [ + column for column in table_config.exclude_columns + if column in chunk.columns + ] + if not exclude_columns: + return chunk + + return chunk.drop(columns=exclude_columns) + + def split_incremental_chunk( + self, + chunk: pd.DataFrame, + table_config: TableMigrationConfig, + ) -> Tuple[pd.DataFrame, pd.DataFrame]: + """Разделение Life_ чанка на строки для upsert и delete.""" + if not table_config.operation_column: + return chunk, chunk.iloc[0:0].copy() + + if table_config.operation_column not in chunk.columns: + raise ValueError( + f"В таблице {table_config.read_table} не найдено поле {table_config.operation_column}" + ) + + operations = chunk[table_config.operation_column].astype(str).str.lower().str.strip() + upsert_operations = {operation.lower() for operation in table_config.upsert_operations} + delete_operations = {operation.lower() for operation in table_config.delete_operations} + + upsert_chunk = chunk[operations.isin(upsert_operations)] + delete_chunk = chunk[operations.isin(delete_operations)] + return upsert_chunk, delete_chunk + + def deduplicate_incremental_chunk( + self, + chunk: pd.DataFrame, + primary_key: List[str], + ) -> pd.DataFrame: + """Схлопывание повторных событий по ключу внутри одного чанка с сохранением последней версии.""" + if chunk.empty or not primary_key: + return chunk + + missing_columns = [column for column in primary_key if column not in chunk.columns] + if missing_columns: + raise ValueError( + f"Для дедупликации не найдены ключевые поля: {missing_columns}" + ) + + before_count = len(chunk) + deduplicated_chunk = chunk.drop_duplicates(subset=primary_key, keep='last') + dropped_rows = before_count - len(deduplicated_chunk) + if dropped_rows > 0: + self.logger.log_info( + f"Схлопнуто {dropped_rows} повторных событий внутри чанка по ключу {primary_key}" + ) + return deduplicated_chunk + + def get_effective_delete_count( + self, + chunk: pd.DataFrame, + primary_key: List[str], + ) -> int: + """Количество фактических delete-операций после схлопывания дублей по ключу.""" + if chunk.empty: + return 0 + if not primary_key: + return len(chunk) + missing_columns = [column for column in primary_key if column not in chunk.columns] + if missing_columns: + raise ValueError( + f"Для расчета delete count не найдены ключевые поля: {missing_columns}" + ) + return len(chunk[primary_key].drop_duplicates()) + + def delete_dataframe_batch( + self, + chunk: pd.DataFrame, + table_name: str, + primary_key: List[str], + ): + """Batch delete в PostgreSQL через staging-таблицу с ключами.""" + if chunk.empty: + return + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено удаление {len(chunk)} строк из {table_name}") + return + if not self.table_exists(table_name): + self.logger.log_warning(f"Удаление пропущено: таблица {table_name} еще не существует") + return + if not primary_key: + raise ValueError(f"Для удаления из {table_name} не задан primary_key") + + missing_columns = [column for column in primary_key if column not in chunk.columns] + if missing_columns: + raise ValueError(f"Для удаления из {table_name} не найдены ключевые поля: {missing_columns}") + + staging_table = f"_stg_delete_{table_name}_{int(time.time() * 1000)}" + key_chunk = chunk[primary_key].drop_duplicates() + join_condition = ' AND '.join([ + f"target.{self.quote_identifier(column)} = source.{self.quote_identifier(column)}" + for column in primary_key + ]) + + try: + self.write_dataframe_batch(key_chunk, staging_table, if_exists='replace') + sql = f""" + DELETE FROM {self.quote_identifier(table_name)} AS target + USING {self.quote_identifier(staging_table)} AS source + WHERE {join_condition} + """ + with self.dst_engine.connect() as conn: + conn.execute(text(sql)) + conn.execute(text(f'DROP TABLE IF EXISTS {self.quote_identifier(staging_table)}')) + conn.commit() + except Exception: + with self.dst_engine.connect() as conn: + conn.execute(text(f'DROP TABLE IF EXISTS {self.quote_identifier(staging_table)}')) + conn.commit() + raise + + def upsert_dataframe_batch( + self, + chunk: pd.DataFrame, + table_name: str, + primary_key: List[str], + ): + """Batch upsert через staging-таблицу.""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущен upsert {len(chunk)} строк в {table_name}") + return + + if not primary_key: + self.write_dataframe_batch(chunk, table_name, if_exists='append') + return + + chunk = self.deduplicate_incremental_chunk(chunk, primary_key) + staging_table = f"_stg_{table_name}_{int(time.time() * 1000)}" + columns = list(chunk.columns) + quoted_columns = ', '.join([self.quote_identifier(column) for column in columns]) + conflict_columns = ', '.join([self.quote_identifier(column) for column in primary_key]) + update_columns = [column for column in columns if column not in primary_key] + + if update_columns: + update_set = ', '.join([ + f"{self.quote_identifier(column)} = EXCLUDED.{self.quote_identifier(column)}" + for column in update_columns + ]) + conflict_action = f"DO UPDATE SET {update_set}" + else: + conflict_action = "DO NOTHING" + + try: + self.write_dataframe_batch(chunk, staging_table, if_exists='replace') + sql = f""" + INSERT INTO {self.quote_identifier(table_name)} ({quoted_columns}) + SELECT {quoted_columns} + FROM {self.quote_identifier(staging_table)} + ON CONFLICT ({conflict_columns}) {conflict_action} + """ + with self.dst_engine.connect() as conn: + conn.execute(text(sql)) + conn.execute(text(f'DROP TABLE IF EXISTS {self.quote_identifier(staging_table)}')) + conn.commit() + except Exception: + with self.dst_engine.connect() as conn: + conn.execute(text(f'DROP TABLE IF EXISTS {self.quote_identifier(staging_table)}')) + conn.commit() + raise + + def create_timescale_hypertable(self, table_config: TableMigrationConfig): + """Преобразование PostgreSQL таблицы в TimescaleDB hypertable.""" + if not self.config.ENABLE_TIMESCALE or not table_config.timescale: + return + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено создание TimescaleDB hypertable для {table_config.pg_table}") + return + + time_column = table_config.timescale_time_column or table_config.datetime_column + table_name = table_config.pg_table + try: + with self.dst_engine.connect() as conn: + conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb")) + conn.execute(text(""" + SELECT create_hypertable( + :table_name, + :time_column, + if_not_exists => TRUE, + migrate_data => TRUE + ) + """), { + 'table_name': table_name, + 'time_column': time_column, + }) + conn.commit() + self.logger.log_info(f"Таблица {table_name} преобразована в TimescaleDB hypertable") + except Exception as e: + self.logger.log_error(f"Ошибка при создании TimescaleDB hypertable для {table_name}", e) + + def can_create_primary_key( + self, + table_config: TableMigrationConfig, + pk_columns: List[str], + ) -> bool: + """Проверка совместимости primary key с TimescaleDB.""" + if not pk_columns: + return False + if not self.config.ENABLE_TIMESCALE or not table_config.timescale: + return True + + time_column = table_config.timescale_time_column or table_config.datetime_column + if time_column in pk_columns: + return True + + self.logger.log_warning( + f"Primary key для {table_config.pg_table} пропущен: TimescaleDB hypertable " + f"требует включить time column {time_column} в уникальные ограничения" + ) + return False + + def get_mssql_indexes(self, table_name: str) -> List[Dict[str, Any]]: + """Получение индексов из MSSQL таблицы""" + cache_key = table_name.lower() + if cache_key in self._mssql_indexes_cache: + return self._mssql_indexes_cache[cache_key] + + indexes = [] + + query = f""" + SELECT + i.name as index_name, + i.is_unique, + i.type_desc as index_type, + c.name as column_name, + ic.key_ordinal as column_position, + ic.is_descending_key + FROM sys.indexes i + INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + INNER JOIN sys.tables t ON i.object_id = t.object_id + WHERE t.name = '{table_name}' + AND i.type_desc NOT IN ('HEAP', 'CLUSTERED') + AND i.is_primary_key = 0 + ORDER BY i.name, ic.key_ordinal + """ + + try: + index_df = pd.read_sql_query(query, self.src_engine) + + if not index_df.empty: + for index_name in index_df['index_name'].unique(): + index_data = index_df[index_df['index_name'] == index_name] + unique = bool(index_data.iloc[0]['is_unique']) + + columns = [] + for _, row in index_data.sort_values('column_position').iterrows(): + column_name = row['column_name'] + column_name = column_name.replace('[', '').replace(']', '') + columns.append(column_name) + + indexes.append({ + 'name': index_name, + 'unique': unique, + 'columns': columns + }) + + self.logger.log_info(f"Найдено {len(indexes)} индексов для таблицы {table_name}") + + except Exception as e: + self.logger.log_error(f"Ошибка при получении индексов для таблицы {table_name}", e) + + self._mssql_indexes_cache[cache_key] = indexes + return indexes + + def create_pg_indexes(self, table_name: str, indexes: List[Dict[str, Any]]): + """Создание индексов в PostgreSQL""" + if not indexes: + return + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено создание {len(indexes)} индексов для {table_name}") + return + + for index_info in indexes: + index_name = f"idx_{table_name}_{index_info['name'].lower()}" + index_name = re.sub(r'[^a-z0-9_]', '_', index_name) + + columns = ', '.join([f'"{col}"' for col in index_info['columns']]) + unique_str = 'UNIQUE ' if index_info['unique'] else '' + + sql = f'CREATE {unique_str}INDEX IF NOT EXISTS "{index_name}" ON "{table_name}" ({columns})' + + try: + with self.dst_engine.connect() as conn: + conn.execute(text(sql)) + conn.commit() + self.logger.log_info(f"Создан индекс: {index_name} на столбцах: {columns}") + except Exception as e: + self.logger.log_error(f"Ошибка при создании индекса {index_name}", e) + + def get_mssql_primary_key(self, table_name: str) -> Optional[List[str]]: + """Получение информации о первичном ключе из MSSQL""" + cache_key = table_name.lower() + if cache_key in self._mssql_pk_cache: + return self._mssql_pk_cache[cache_key] + + query = f""" + SELECT + c.name as column_name + FROM sys.indexes i + INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + INNER JOIN sys.tables t ON i.object_id = t.object_id + WHERE t.name = '{table_name}' + AND i.is_primary_key = 1 + ORDER BY ic.key_ordinal + """ + + try: + pk_df = pd.read_sql_query(query, self.src_engine) + if not pk_df.empty: + pk_columns = [row['column_name'] for _, row in pk_df.iterrows()] + self.logger.log_info(f"Найден первичный ключ для таблицы {table_name}: {pk_columns}") + self._mssql_pk_cache[cache_key] = pk_columns + return pk_columns + except Exception as e: + self.logger.log_error(f"Ошибка при получении первичного ключа для таблицы {table_name}", e) + self._mssql_pk_cache[cache_key] = None + return None + + def create_pg_primary_key(self, table_name: str, pk_columns: List[str]): + """Создание первичного ключа в PostgreSQL""" + if not pk_columns: + return + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено создание первичного ключа для {table_name}") + return + + columns = ', '.join([f'"{col}"' for col in pk_columns]) + pk_name = f"pk_{table_name}" + + sql = f'ALTER TABLE "{table_name}" ADD CONSTRAINT "{pk_name}" PRIMARY KEY ({columns})' + + try: + with self.dst_engine.connect() as conn: + conn.execute(text(sql)) + conn.commit() + self.logger.log_info(f"Создан первичный ключ: {pk_name} на столбцах: {columns}") + except Exception as e: + self.logger.log_error(f"Ошибка при создании первичного ключа {pk_name}", e) + + def has_pg_primary_key(self, table_name: str) -> bool: + """Проверка наличия primary key в PostgreSQL.""" + sql = text(""" + SELECT EXISTS ( + SELECT 1 + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE c.contype = 'p' + AND n.nspname = current_schema() + AND t.relname = :table_name + ) + """) + with self.dst_engine.connect() as conn: + return bool(conn.execute(sql, {'table_name': table_name}).scalar()) + + def ensure_pg_primary_key(self, table_name: str, pk_columns: List[str]): + """Создание primary key, если его еще нет.""" + if not pk_columns: + return + if self.has_pg_primary_key(table_name): + return + self.create_pg_primary_key(table_name, pk_columns) + + def get_mssql_foreign_keys(self, table_name: str) -> Optional[List[Dict[str, str]]]: + """Получение информации о внешних ключах из MSSQL""" + cache_key = table_name.lower() + if cache_key in self._mssql_fk_cache: + return self._mssql_fk_cache[cache_key] + + query = f""" + SELECT + fk.name as fk_name, + pc.name as parent_column, + rc.name as referenced_column, + rt.name as referenced_table + FROM sys.foreign_keys fk + INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id + INNER JOIN sys.columns pc ON fkc.parent_object_id = pc.object_id AND fkc.parent_column_id = pc.column_id + INNER JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id + INNER JOIN sys.tables rt ON fkc.referenced_object_id = rt.object_id + INNER JOIN sys.tables pt ON fkc.parent_object_id = pt.object_id + WHERE pt.name = '{table_name}' + ORDER BY fk.name, fkc.constraint_column_id + """ + + try: + fk_df = pd.read_sql_query(query, self.src_engine) + if not fk_df.empty: + fks = [] + for fk_name in fk_df['fk_name'].unique(): + fk_data = fk_df[fk_df['fk_name'] == fk_name] + fks.append({ + 'name': fk_name, + 'parent_column': fk_data.iloc[0]['parent_column'], + 'referenced_table': fk_data.iloc[0]['referenced_table'].lower(), + 'referenced_column': fk_data.iloc[0]['referenced_column'] + }) + self.logger.log_info(f"Найдено {len(fks)} внешних ключей для таблицы {table_name}") + self._mssql_fk_cache[cache_key] = fks + return fks + except Exception as e: + self.logger.log_error(f"Ошибка при получении внешних ключей для таблицы {table_name}", e) + self._mssql_fk_cache[cache_key] = None + return None + + def create_pg_foreign_keys(self, table_name: str, foreign_keys: List[Dict[str, str]]): + """Создание внешних ключей в PostgreSQL""" + if not foreign_keys: + return + if not self.config.CREATE_FOREIGN_KEYS: + self.logger.log_info(f"Создание внешних ключей отключено конфигом для {table_name}") + return + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено создание {len(foreign_keys)} внешних ключей для {table_name}") + return + + for fk_info in foreign_keys: + if not self.table_exists(fk_info['referenced_table']): + self.logger.log_warning( + f"Внешний ключ для {table_name}.{fk_info['parent_column']} пропущен: " + f"таблица {fk_info['referenced_table']} отсутствует в PostgreSQL" + ) + continue + fk_name = f"fk_{table_name}_{fk_info['parent_column']}" + + sql = f""" + ALTER TABLE "{table_name}" + ADD CONSTRAINT "{fk_name}" + FOREIGN KEY ("{fk_info['parent_column']}") + REFERENCES "{fk_info['referenced_table']}" ("{fk_info['referenced_column']}") + """ + + try: + with self.dst_engine.connect() as conn: + conn.execute(text(sql)) + conn.commit() + self.logger.log_info(f"Создан внешний ключ: {fk_name}") + except Exception as e: + self.logger.log_error(f"Ошибка при создании внешнего ключа {fk_name}", e) + + def analyze_table(self, table_name: str): + """Выполнение ANALYZE для таблицы""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущен ANALYZE для {table_name}") + return + try: + with self.dst_engine.connect() as conn: + sql = f'ANALYZE "{table_name}"' + conn.execute(text(sql)) + conn.commit() + self.logger.log_info(f"Выполнен ANALYZE для таблицы {table_name}") + except Exception as e: + self.logger.log_error(f"Ошибка при выполнении ANALYZE для таблицы {table_name}", e) + + def vacuum_analyze_table(self, table_name: str): + """Выполнение VACUUM ANALYZE для таблицы""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущен VACUUM ANALYZE для {table_name}") + return + try: + with self.dst_engine.connect() as conn: + sql = f'VACUUM ANALYZE "{table_name}"' + conn.execute(text(sql)) + conn.commit() + self.logger.log_info(f"Выполнен VACUUM ANALYZE для таблицы {table_name}") + except Exception as e: + self.logger.log_error(f"Ошибка при выполнении VACUUM ANALYZE для таблицы {table_name}", e) + + def update_table_statistics(self): + """Обновление статистики для всей базы данных""" + if self.config.DRY_RUN: + self.logger.log_info("DRY RUN: пропущен ANALYZE для всей базы данных") + return + try: + with self.dst_engine.connect() as conn: + conn.execute(text('ANALYZE')) + conn.commit() + self.logger.log_info("Выполнен ANALYZE для всей базы данных") + except Exception as e: + self.logger.log_error("Ошибка при выполнении ANALYZE для всей базы данных", e) + + def truncate_table(self, table_name: str): + """Очистка таблицы""" + if self.config.DRY_RUN: + self.logger.log_info(f"DRY RUN: пропущено удаление таблицы {table_name}") + return + + table_exists = self.table_exists(table_name) + + if table_exists: + try: + query = text(f'DROP TABLE IF EXISTS {self.quote_identifier(table_name)} CASCADE') + with self.dst_engine.connect() as connection: + connection.execute(query) + connection.commit() + self.logger.log_info(f"Таблица {table_name} удалена") + except Exception as e: + self.logger.log_error(f"Ошибка при удалении таблицы {table_name}", e) + else: + self.logger.log_info(f"Таблица {table_name} не существует в целевой БД") + + def migrate_table(self, table: Any, force_full: bool = False) -> bool: + """Миграция одной таблицы""" + table_config = table if isinstance(table, TableMigrationConfig) else self.get_table_config(table) + max_attempts = max(1, self.config.MSSQL_TABLE_RETRIES + 1) + + for attempt in range(1, max_attempts + 1): + try: + return self.migrate_table_once(table_config, force_full=force_full) + except Exception as e: + if not self.is_retryable_mssql_error(e) or attempt >= max_attempts: + raise + + self.logger.log_warning( + f"Временная ошибка MSSQL при обработке {table_config.source_table}, " + f"попытка {attempt} из {max_attempts}: {e}" + ) + self.reconnect_mssql_engine() + time.sleep(min(attempt * 5, 15)) + + return False + + def migrate_full_table( + self, + table_config: TableMigrationConfig, + read_limit: Optional[int] = None, + ) -> bool: + """Полная миграция одной таблицы с пересозданием целевой таблицы.""" + table_name = table_config.source_table + pg_table = table_config.pg_table + self.logger.log_table_start(table_name) + + try: + # Получаем метаданные + indexes = self.get_mssql_indexes(table_name) + pk_columns = self.get_mssql_primary_key(table_name) + foreign_keys = self.get_mssql_foreign_keys(table_name) + + # Очищаем целевую таблицу + self.logger.log_info(f"Очистка целевой таблицы {pg_table}") + self.truncate_table(pg_table) + + # Читаем данные + self.logger.log_info(f"Чтение данных из {table_name}") + chunks = self.read_full_chunks(table_name, read_limit=read_limit) + + # Загружаем данные + first_chunk = True + total_rows = 0 + + for chunk_num, chunk in enumerate(chunks, 1): + if first_chunk: + self.write_dataframe_batch(chunk, pg_table, if_exists='fail') + first_chunk = False + self.logger.log_info(f"Таблица {pg_table} создана") + else: + self.write_dataframe_batch(chunk, pg_table, if_exists='append') + + total_rows += len(chunk) + if chunk_num % self.config.BATCH_SIZE == 0: + self.logger.log_progress(table_name, chunk_num, total_rows) + + self.logger.log_info(f"Всего загружено строк: {total_rows}") + + if total_rows > 0: + self.sync_target_schema(table_name, pg_table) + + if total_rows > 0: + self.create_timescale_hypertable(table_config) + + # Создаем первичный ключ + if self.can_create_primary_key(table_config, pk_columns): + self.logger.log_info(f"Создание первичного ключа для {pg_table}") + self.create_pg_primary_key(pg_table, pk_columns) + + # Создаем индексы + if indexes: + self.logger.log_info(f"Создание {len(indexes)} индексов для {pg_table}") + self.create_pg_indexes(pg_table, indexes) + + # Создаем внешние ключи + if foreign_keys: + self.logger.log_info(f"Создание {len(foreign_keys)} внешних ключей для {pg_table}") + self.create_pg_foreign_keys(pg_table, foreign_keys) + + # Обновляем статистику + self.logger.log_info(f"Обновление статистики для {pg_table}") + if total_rows > 1000000: + self.vacuum_analyze_table(pg_table) + else: + self.analyze_table(pg_table) + + self.logger.log_table_success(table_name, total_rows) + return True + + except Exception as e: + if self.is_retryable_mssql_error(e): + raise + self.logger.log_table_failure(table_name, str(e)) + return False + + def migrate_incremental_table(self, table_config: TableMigrationConfig) -> bool: + """Инкрементальная миграция Life_ таблицы по x_DateTime.""" + table_name = table_config.source_table + pg_table = table_config.pg_table + self.logger.log_table_start(f"{table_name} ({table_config.read_table})") + + try: + if not table_config.life_table: + raise ValueError(f"Для инкрементальной миграции {table_name} не задана life_table") + + last_watermark = self.get_last_watermark(pg_table) + upper_bound = self.get_incremental_upper_bound(table_config) + target_exists = self.table_exists(pg_table) + + if upper_bound['last_x_datetime'] is None: + self.logger.log_info(f"В {table_config.read_table} нет данных для инкрементальной миграции") + self.save_watermark(pg_table, last_watermark['last_x_datetime'], last_watermark['last_sequence_value'], 0, 'success') + self.logger.log_table_success(table_name, 0) + return True + + if ( + table_config.initial_load_mode == 'full_then_incremental' + and not target_exists + and last_watermark['last_x_datetime'] is None + ): + self.logger.log_info( + f"Первичная загрузка {table_name}: full snapshot из {table_config.source_table}, " + f"затем watermark по {table_config.read_table}" + ) + success = self.migrate_full_table(table_config, read_limit=self.config.READ_LIMIT) + if success: + self.save_watermark( + pg_table, + upper_bound['last_x_datetime'], + upper_bound['last_sequence_value'], + 0, + 'success', + ) + return success + + self.logger.log_info( + f"Инкрементальная миграция {table_config.read_table}: " + f"{table_config.datetime_column} > {last_watermark}, <= {upper_bound}" + ) + + first_chunk = not target_exists + total_rows = 0 + total_events = 0 + max_seen_watermark = dict(last_watermark) + chunks = self.read_incremental_chunks( + table_config, + last_watermark, + upper_bound, + read_limit=self.config.READ_LIMIT, + ) + + if target_exists: + self.sync_target_schema(table_name, pg_table) + + if table_config.primary_key and target_exists and self.can_create_primary_key(table_config, table_config.primary_key): + self.ensure_pg_primary_key(pg_table, table_config.primary_key) + + for chunk_num, chunk in enumerate(chunks, 1): + if chunk.empty: + continue + + if table_config.datetime_column not in chunk.columns: + raise ValueError( + f"В таблице {table_config.read_table} не найдено поле {table_config.datetime_column}" + ) + + upsert_chunk, delete_chunk = self.split_incremental_chunk(chunk, table_config) + write_chunk = self.prepare_incremental_chunk(upsert_chunk, table_config) + write_chunk = self.deduplicate_incremental_chunk(write_chunk, table_config.primary_key) + delete_count = self.get_effective_delete_count(delete_chunk, table_config.primary_key) + missing_pk_columns = [ + column for column in table_config.primary_key + if column not in write_chunk.columns + ] + if not write_chunk.empty and missing_pk_columns: + raise ValueError( + f"После исключения служебных полей отсутствуют ключевые поля: {missing_pk_columns}" + ) + + if not delete_chunk.empty: + self.delete_dataframe_batch(delete_chunk, pg_table, table_config.primary_key) + + if write_chunk.empty: + pass + elif first_chunk: + self.write_dataframe_batch(write_chunk, pg_table, if_exists='append') + self.create_timescale_hypertable(table_config) + if self.can_create_primary_key(table_config, table_config.primary_key): + self.create_pg_primary_key(pg_table, table_config.primary_key) + first_chunk = False + elif table_config.primary_key: + self.upsert_dataframe_batch(write_chunk, pg_table, table_config.primary_key) + else: + self.write_dataframe_batch(write_chunk, pg_table, if_exists='append') + + total_events += len(chunk) + total_rows += len(write_chunk) + delete_count + last_row = chunk.iloc[-1] + max_seen_watermark = { + 'last_x_datetime': last_row[table_config.datetime_column], + 'last_sequence_value': ( + int(last_row[table_config.sequence_column]) + if table_config.sequence_column and pd.notna(last_row[table_config.sequence_column]) + else None + ), + } + + if chunk_num % self.config.BATCH_SIZE == 0: + self.logger.log_progress(table_name, chunk_num, total_rows) + + if total_rows == 0: + max_seen_watermark = upper_bound + + self.save_watermark( + pg_table, + max_seen_watermark['last_x_datetime'], + max_seen_watermark['last_sequence_value'], + total_rows, + 'success', + ) + + self.logger.log_info(f"Всего инкрементально загружено строк: {total_rows}") + if total_events != total_rows: + self.logger.log_info( + f"Получено событий из Life_: {total_events}, фактически применено изменений: {total_rows}" + ) + if total_rows > 1000000: + self.vacuum_analyze_table(pg_table) + elif total_rows > 0: + self.analyze_table(pg_table) + + self.logger.log_table_success(table_name, total_rows) + return True + + except Exception as e: + if self.is_retryable_mssql_error(e): + raise + try: + failed_watermark = self.get_last_watermark(pg_table) + self.save_watermark( + pg_table, + failed_watermark['last_x_datetime'], + failed_watermark['last_sequence_value'], + 0, + 'failed', + str(e), + ) + except Exception as state_error: + self.logger.log_error(f"Ошибка при сохранении состояния миграции {pg_table}", state_error) + self.logger.log_table_failure(table_name, str(e)) + return False + + def run_migration( + self, + table_names: Optional[List[str]] = None, + send_email: bool = False, + dry_run: Optional[bool] = None, + read_limit: Optional[int] = None, + force_full: bool = False, + ): + """Запуск полной миграции""" + if dry_run is not None: + self.config.DRY_RUN = dry_run + if read_limit is not None: + self.config.READ_LIMIT = read_limit + + table_configs = self.table_configs + if table_names: + requested = {table_name.lower() for table_name in table_names} + table_configs = [ + table_config for table_config in self.table_configs + if table_config.source_table.lower() in requested or table_config.pg_table in requested + ] + self.logger.stats['total_tables'] = len(table_configs) + + self.logger.log_info("="*60) + self.logger.log_info("НАЧАЛО МИГРАЦИИ ДАННЫХ") + self.logger.log_info(f"Время начала: {self.logger.start_time}") + self.logger.log_info(f"Количество таблиц для обработки: {len(table_configs)}") + self.logger.log_info(f"Force full reload: {force_full}") + self.logger.log_info("="*60) + + # Миграция таблиц + for table_config in table_configs: + success = self.migrate_table(table_config, force_full=force_full) + if not success: + self.logger.log_warning(f"Таблица {table_config.source_table} пропущена из-за ошибки") + + # Финальная статистика + self.logger.log_info("Обновление статистики для всей базы данных...") + self.update_table_statistics() + + # Генерация отчета + report = self.logger.generate_report() + + # Вывод итогов + self.logger.log_info("="*60) + self.logger.log_info("ИТОГОВАЯ СТАТИСТИКА") + self.logger.log_info("="*60) + self.logger.log_info(f"Успешно скопировано таблиц: {report['summary']['successful_tables']}") + self.logger.log_info(f"Не удалось скопировать таблиц: {report['summary']['failed_tables']}") + self.logger.log_info(f"Всего строк: {report['summary']['total_rows']}") + self.logger.log_info(f"Процент успеха: {report['summary']['success_rate']:.2f}%") + self.logger.log_info(f"Продолжительность: {report['summary']['duration']}") + self.logger.log_info("="*60) + + # Отправка email + if send_email: + self.send_notification(report) + else: + self.logger.log_info("Email отключен для этого запуска") + + return report + + def backfill_watermarks( + self, + table_names: Optional[List[str]] = None, + overwrite: bool = False, + dry_run: bool = False, + ) -> Dict[str, Any]: + """Заполнение migration_state верхними границами Life_ без перезаливки данных.""" + original_dry_run = self.config.DRY_RUN + self.config.DRY_RUN = dry_run + + try: + table_configs = self.table_configs + if table_names: + requested = {table_name.lower() for table_name in table_names} + table_configs = [ + table_config for table_config in self.table_configs + if table_config.source_table.lower() in requested or table_config.pg_table in requested + ] + + summary = { + 'processed': 0, + 'updated': 0, + 'skipped': 0, + 'failed': 0, + 'tables': [], + } + + self.logger.log_info("=" * 60) + self.logger.log_info("НАЧАЛО BACKFILL WATERMARKS") + self.logger.log_info(f"Количество таблиц для обработки: {len(table_configs)}") + self.logger.log_info(f"Overwrite existing: {overwrite}") + self.logger.log_info(f"Dry run: {dry_run}") + self.logger.log_info("=" * 60) + + for table_config in table_configs: + summary['processed'] += 1 + table_name = table_config.source_table + pg_table = table_config.pg_table + + if table_config.mode != 'incremental' or not table_config.life_table: + self.logger.log_info( + f"Пропуск {table_name}: таблица не настроена на incremental через Life_" + ) + summary['skipped'] += 1 + summary['tables'].append({ + 'table': table_name, + 'status': 'skipped', + 'reason': 'not_incremental', + }) + continue + + existing_watermark = self.get_last_watermark(pg_table) + if existing_watermark['last_x_datetime'] is not None and not overwrite: + self.logger.log_info( + f"Пропуск {table_name}: watermark уже существует для {pg_table}" + ) + summary['skipped'] += 1 + summary['tables'].append({ + 'table': table_name, + 'status': 'skipped', + 'reason': 'watermark_exists', + 'watermark': existing_watermark, + }) + continue + + try: + upper_bound = self.get_incremental_upper_bound(table_config) + if upper_bound['last_x_datetime'] is None: + self.logger.log_warning( + f"Пропуск {table_name}: в {table_config.life_table} нет данных" + ) + summary['skipped'] += 1 + summary['tables'].append({ + 'table': table_name, + 'status': 'skipped', + 'reason': 'life_empty', + }) + continue + + self.save_watermark( + pg_table, + upper_bound['last_x_datetime'], + upper_bound['last_sequence_value'], + 0, + 'success', + ) + self.logger.log_info( + f"Watermark сохранен для {pg_table}: {upper_bound}" + ) + summary['updated'] += 1 + summary['tables'].append({ + 'table': table_name, + 'status': 'updated', + 'watermark': upper_bound, + }) + except Exception as e: + self.logger.log_error( + f"Ошибка backfill watermark для {table_name}", + e, + ) + summary['failed'] += 1 + summary['tables'].append({ + 'table': table_name, + 'status': 'failed', + 'error': str(e), + }) + + self.logger.log_info("=" * 60) + self.logger.log_info("ИТОГ BACKFILL WATERMARKS") + self.logger.log_info( + f"Обработано: {summary['processed']}, обновлено: {summary['updated']}, " + f"пропущено: {summary['skipped']}, ошибок: {summary['failed']}" + ) + self.logger.log_info("=" * 60) + return summary + finally: + self.config.DRY_RUN = original_dry_run + + def send_notification(self, report: Dict[str, Any]): + """Отправка уведомления по email""" + # Формируем тело письма + subject = f"{self.config.EMAIL_SUBJECT} - {datetime.now().strftime('%Y-%m-%d %H:%M')}" + + body = f""" +Результат миграции данных MSSQL → PostgreSQL + +ОБЩАЯ ИНФОРМАЦИЯ: +------------------ +Время начала: {report['summary']['start_time']} +Время окончания: {report['summary']['end_time']} +Продолжительность: {report['summary']['duration']} + +СТАТИСТИКА: +----------- +Всего таблиц: {report['summary']['total_tables']} +Успешно скопировано: {report['summary']['successful_tables']} +Не удалось скопировать: {report['summary']['failed_tables']} +Процент успеха: {report['summary']['success_rate']:.2f}% +Всего строк: {report['summary']['total_rows']} + +УСПЕШНЫЕ ТАБЛИЦЫ ({len(report['successful_tables'])}): +------------------- +{chr(10).join([f"- {t['name']}: {t['rows']} строк" for t in report['successful_tables']])} + +ПРОВАЛЕННЫЕ ТАБЛИЦЫ ({len(report['failed_tables'])}): +--------------------- +{chr(10).join([f"- {t['name']}: {t['error']}" for t in report['failed_tables']])} + +ОШИБКИ ({len(report['errors'])}): +-------- +{chr(10).join([f"- {e['message']}" for e in report['errors'][:5]])} +{"..." if len(report['errors']) > 5 else ""} + +Лог-файл: {self.logger.log_file} +Отчет в формате JSON также прикреплен к письму. +""" + + # Вложения + attachments = [ + self.logger.log_file, + os.path.join(self.config.LOG_DIR, f"migration_report_{self.logger.timestamp}.json") + ] + + # Отправка + self.email_sender.send_email(subject, body, attachments) + + def send_failure_notification( + self, + error: str, + table_names: Optional[List[str]] = None, + job_id: Optional[str] = None, + ): + """Отправка уведомления о провале job до формирования стандартного отчета.""" + subject = f"{self.config.EMAIL_SUBJECT} - FAILED - {datetime.now().strftime('%Y-%m-%d %H:%M')}" + tables_text = ', '.join(table_names) if table_names else 'all configured tables' + body = f""" +Результат миграции данных MSSQL → PostgreSQL + +СТАТУС: +------- +Выполнение job завершилось ошибкой до формирования итогового отчета. + +ДЕТАЛИ: +------- +Job ID: {job_id or 'n/a'} +Таблицы: {tables_text} +Ошибка: {error} + +Лог-файл: {self.logger.log_file} +""" + + attachments = [self.logger.log_file] + self.email_sender.send_email(subject, body, attachments) + + def cleanup_old_logs(self, days_to_keep: int = 7): + """Очистка старых логов""" + try: + cutoff_date = datetime.now().timestamp() - (days_to_keep * 24 * 3600) + + for filename in os.listdir(self.config.LOG_DIR): + filepath = os.path.join(self.config.LOG_DIR, filename) + + # Проверяем, что это лог-файл + if filename.endswith('.log') or filename.endswith('.json'): + # Получаем время последнего изменения + file_mtime = os.path.getmtime(filepath) + + # Удаляем старые файлы + if file_mtime < cutoff_date: + os.remove(filepath) + self.logger.log_info(f"Удален старый файл: {filename}") + + except Exception as e: + self.logger.log_error("Ошибка при очистке старых логов", e) diff --git a/app/queue.py b/app/queue.py new file mode 100644 index 0000000..5a9bfcb --- /dev/null +++ b/app/queue.py @@ -0,0 +1,692 @@ +import json +import threading +import time +import uuid +from dataclasses import dataclass +from datetime import datetime, timedelta, time as dt_time +from typing import Any, Dict, List, Optional + +from .config import Config + + +@dataclass +class MigrationJob: + """Сериализуемое представление задания миграции.""" + job_id: str + created_at: datetime + run_at: datetime + tables: Optional[List[str]] + send_email: bool + dry_run: Optional[bool] + read_limit: Optional[int] + force_full: bool + status: str + started_at: Optional[datetime] + finished_at: Optional[datetime] + report: Optional[Dict[str, Any]] + error: Optional[str] + queue_sequence: int + schedule_id: Optional[str] + + def to_dict(self) -> Dict[str, Any]: + return { + 'job_id': self.job_id, + 'created_at': self.created_at.isoformat(), + 'run_at': self.run_at.isoformat(), + 'tables': self.tables, + 'send_email': self.send_email, + 'dry_run': self.dry_run, + 'read_limit': self.read_limit, + 'force_full': self.force_full, + 'status': self.status, + 'started_at': self.started_at.isoformat() if self.started_at else None, + 'finished_at': self.finished_at.isoformat() if self.finished_at else None, + 'report': self.report, + 'error': self.error, + 'queue_sequence': self.queue_sequence, + 'schedule_id': self.schedule_id, + } + + +@dataclass +class MigrationSchedule: + """Сериализуемое представление расписания.""" + schedule_id: str + created_at: datetime + updated_at: datetime + name: Optional[str] + schedule_type: str + enabled: bool + catch_up_missed_runs: bool + initial_force_full: bool + tables: Optional[List[str]] + send_email: bool + dry_run: Optional[bool] + read_limit: Optional[int] + interval_seconds: Optional[int] + daily_time: Optional[str] + start_at: Optional[datetime] + next_run_at: datetime + last_enqueued_at: Optional[datetime] + last_job_id: Optional[str] + + def to_dict(self) -> Dict[str, Any]: + return { + 'schedule_id': self.schedule_id, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'name': self.name, + 'schedule_type': self.schedule_type, + 'enabled': self.enabled, + 'catch_up_missed_runs': self.catch_up_missed_runs, + 'initial_force_full': self.initial_force_full, + 'tables': self.tables, + 'send_email': self.send_email, + 'dry_run': self.dry_run, + 'read_limit': self.read_limit, + 'interval_seconds': self.interval_seconds, + 'daily_time': self.daily_time, + 'start_at': self.start_at.isoformat() if self.start_at else None, + 'next_run_at': self.next_run_at.isoformat(), + 'last_enqueued_at': self.last_enqueued_at.isoformat() if self.last_enqueued_at else None, + 'last_job_id': self.last_job_id, + } + + +class MigrationJobQueue: + """Persistent очередь и планировщик миграций поверх PostgreSQL.""" + + JOBS_TABLE = 'migration_jobs' + SCHEDULES_TABLE = 'migration_schedules' + + def __init__(self): + self.lock = threading.Lock() + self.condition = threading.Condition(self.lock) + self.engine = None + self.schema_ready = False + self.worker_started = False + self.worker = None + + def start(self): + """Ленивая инициализация worker thread.""" + with self.condition: + if self.worker_started: + return + self.worker_started = True + self.worker = threading.Thread(target=self._worker_loop, daemon=True) + self.worker.start() + self.condition.notify_all() + + def enqueue( + self, + tables: Optional[List[str]] = None, + send_email: bool = True, + dry_run: Optional[bool] = None, + read_limit: Optional[int] = None, + force_full: bool = False, + run_at: Optional[datetime] = None, + delay_seconds: Optional[int] = None, + schedule_id: Optional[str] = None, + ) -> Dict[str, Any]: + self.start() + self._ensure_schema() + scheduled_at = self._resolve_run_at(run_at=run_at, delay_seconds=delay_seconds) + job_id = str(uuid.uuid4()) + created_at = datetime.now() + + sql = self._text(f""" + INSERT INTO {self._qualified_table(self.JOBS_TABLE)} + (job_id, schedule_id, created_at, run_at, tables_json, send_email, dry_run, read_limit, force_full, status) + VALUES + (:job_id, :schedule_id, :created_at, :run_at, CAST(:tables_json AS jsonb), :send_email, :dry_run, :read_limit, :force_full, 'queued') + RETURNING * + """) + with self._get_engine().connect() as conn: + row = conn.execute(sql, { + 'job_id': job_id, + 'schedule_id': schedule_id, + 'created_at': created_at, + 'run_at': scheduled_at, + 'tables_json': json.dumps(tables) if tables is not None else None, + 'send_email': send_email, + 'dry_run': dry_run, + 'read_limit': read_limit, + 'force_full': force_full, + }).mappings().first() + conn.commit() + + with self.condition: + self.condition.notify_all() + + return self._job_from_row(row).to_dict() + + def list_jobs(self, limit: int = 100) -> List[Dict[str, Any]]: + self.start() + self._ensure_schema() + sql = self._text(f""" + SELECT * + FROM {self._qualified_table(self.JOBS_TABLE)} + ORDER BY queue_sequence DESC + LIMIT :limit + """) + with self._get_engine().connect() as conn: + rows = conn.execute(sql, {'limit': limit}).mappings().all() + return [self._job_from_row(row).to_dict() for row in rows] + + def get_job(self, job_id: str) -> Optional[Dict[str, Any]]: + self.start() + self._ensure_schema() + sql = self._text(f"SELECT * FROM {self._qualified_table(self.JOBS_TABLE)} WHERE job_id = :job_id") + with self._get_engine().connect() as conn: + row = conn.execute(sql, {'job_id': job_id}).mappings().first() + return self._job_from_row(row).to_dict() if row else None + + def create_schedule( + self, + schedule_type: str, + tables: Optional[List[str]] = None, + send_email: bool = True, + dry_run: Optional[bool] = None, + read_limit: Optional[int] = None, + interval_seconds: Optional[int] = None, + daily_time: Optional[str] = None, + start_at: Optional[datetime] = None, + name: Optional[str] = None, + enabled: bool = True, + catch_up_missed_runs: bool = False, + initial_force_full: bool = False, + ) -> Dict[str, Any]: + self.start() + self._ensure_schema() + schedule_id = str(uuid.uuid4()) + created_at = datetime.now() + normalized_start_at = self._normalize_datetime(start_at) + next_run_at = self._resolve_next_schedule_run_at( + schedule_type=schedule_type, + interval_seconds=interval_seconds, + daily_time=daily_time, + start_at=normalized_start_at, + reference=created_at, + ) + + sql = self._text(f""" + INSERT INTO {self._qualified_table(self.SCHEDULES_TABLE)} + ( + schedule_id, created_at, updated_at, name, schedule_type, enabled, + catch_up_missed_runs, initial_force_full, tables_json, send_email, dry_run, read_limit, interval_seconds, + daily_time, start_at, next_run_at + ) + VALUES + ( + :schedule_id, :created_at, :updated_at, :name, :schedule_type, :enabled, + :catch_up_missed_runs, :initial_force_full, + CAST(:tables_json AS jsonb), :send_email, :dry_run, :read_limit, :interval_seconds, + :daily_time, :start_at, :next_run_at + ) + RETURNING * + """) + with self._get_engine().connect() as conn: + row = conn.execute(sql, { + 'schedule_id': schedule_id, + 'created_at': created_at, + 'updated_at': created_at, + 'name': name, + 'schedule_type': schedule_type, + 'enabled': enabled, + 'catch_up_missed_runs': catch_up_missed_runs, + 'initial_force_full': initial_force_full, + 'tables_json': json.dumps(tables) if tables is not None else None, + 'send_email': send_email, + 'dry_run': dry_run, + 'read_limit': read_limit, + 'interval_seconds': interval_seconds, + 'daily_time': daily_time, + 'start_at': normalized_start_at, + 'next_run_at': next_run_at, + }).mappings().first() + conn.commit() + + with self.condition: + self.condition.notify_all() + + return self._schedule_from_row(row).to_dict() + + def list_schedules(self) -> List[Dict[str, Any]]: + self.start() + self._ensure_schema() + sql = self._text(f"SELECT * FROM {self._qualified_table(self.SCHEDULES_TABLE)} ORDER BY next_run_at, created_at") + with self._get_engine().connect() as conn: + rows = conn.execute(sql).mappings().all() + return [self._schedule_from_row(row).to_dict() for row in rows] + + def get_schedule(self, schedule_id: str) -> Optional[Dict[str, Any]]: + self.start() + self._ensure_schema() + sql = self._text(f"SELECT * FROM {self._qualified_table(self.SCHEDULES_TABLE)} WHERE schedule_id = :schedule_id") + with self._get_engine().connect() as conn: + row = conn.execute(sql, {'schedule_id': schedule_id}).mappings().first() + return self._schedule_from_row(row).to_dict() if row else None + + def get_status(self) -> Dict[str, Any]: + self.start() + self._ensure_schema() + with self._get_engine().connect() as conn: + running_job = conn.execute(self._text(f""" + SELECT * + FROM {self._qualified_table(self.JOBS_TABLE)} + WHERE status = 'running' + ORDER BY started_at DESC + LIMIT 1 + """)).mappings().first() + queued_jobs = conn.execute(self._text(f""" + SELECT COUNT(*) + FROM {self._qualified_table(self.JOBS_TABLE)} + WHERE status = 'queued' + """)).scalar() + schedules = conn.execute(self._text(f""" + SELECT COUNT(*) + FROM {self._qualified_table(self.SCHEDULES_TABLE)} + WHERE enabled = TRUE + """)).scalar() + + return { + 'running': running_job is not None, + 'running_job': self._job_from_row(running_job).to_dict() if running_job else None, + 'queued_jobs': int(queued_jobs or 0), + 'enabled_schedules': int(schedules or 0), + } + + def _get_engine(self): + if self.engine is None: + from sqlalchemy import create_engine + + self.engine = create_engine(Config.POSTGRES_CONNECTION_STRING) + return self.engine + + def _text(self, sql: str): + from sqlalchemy import text + + return text(sql) + + def _quote_identifier(self, identifier: str) -> str: + return '"' + identifier.replace('"', '""') + '"' + + def _qualified_table(self, table_name: str) -> str: + return f'{self._quote_identifier(Config.REPLICATOR_SCHEMA)}.{self._quote_identifier(table_name)}' + + def _ensure_schema(self): + if self.schema_ready: + return + + with self.lock: + if self.schema_ready: + return + + with self._get_engine().connect() as conn: + conn.execute(self._text( + f'CREATE SCHEMA IF NOT EXISTS {self._quote_identifier(Config.REPLICATOR_SCHEMA)}' + )) + conn.execute(self._text(f""" + CREATE TABLE IF NOT EXISTS {self._qualified_table(self.JOBS_TABLE)} ( + job_id text PRIMARY KEY, + schedule_id text NULL, + created_at timestamp NOT NULL, + run_at timestamp NOT NULL, + tables_json jsonb NULL, + send_email boolean NOT NULL DEFAULT TRUE, + dry_run boolean NULL, + read_limit integer NULL, + force_full boolean NOT NULL DEFAULT FALSE, + status text NOT NULL, + started_at timestamp NULL, + finished_at timestamp NULL, + report_json jsonb NULL, + error text NULL, + queue_sequence bigint GENERATED ALWAYS AS IDENTITY + ) + """)) + conn.execute(self._text(f""" + CREATE INDEX IF NOT EXISTS idx_{self.JOBS_TABLE}_status_run_at + ON {self._qualified_table(self.JOBS_TABLE)} (status, run_at, queue_sequence) + """)) + conn.execute(self._text(f""" + CREATE TABLE IF NOT EXISTS {self._qualified_table(self.SCHEDULES_TABLE)} ( + schedule_id text PRIMARY KEY, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL, + name text NULL, + schedule_type text NOT NULL, + enabled boolean NOT NULL DEFAULT TRUE, + catch_up_missed_runs boolean NOT NULL DEFAULT FALSE, + initial_force_full boolean NOT NULL DEFAULT FALSE, + tables_json jsonb NULL, + send_email boolean NOT NULL DEFAULT TRUE, + dry_run boolean NULL, + read_limit integer NULL, + interval_seconds integer NULL, + daily_time text NULL, + start_at timestamp NULL, + next_run_at timestamp NOT NULL, + last_enqueued_at timestamp NULL, + last_job_id text NULL + ) + """)) + conn.execute(self._text(f""" + ALTER TABLE {self._qualified_table(self.SCHEDULES_TABLE)} + ADD COLUMN IF NOT EXISTS catch_up_missed_runs boolean NOT NULL DEFAULT FALSE + """)) + conn.execute(self._text(f""" + ALTER TABLE {self._qualified_table(self.SCHEDULES_TABLE)} + ADD COLUMN IF NOT EXISTS initial_force_full boolean NOT NULL DEFAULT FALSE + """)) + conn.execute(self._text(f""" + ALTER TABLE {self._qualified_table(self.JOBS_TABLE)} + ADD COLUMN IF NOT EXISTS force_full boolean NOT NULL DEFAULT FALSE + """)) + conn.execute(self._text(f""" + CREATE INDEX IF NOT EXISTS idx_{self.SCHEDULES_TABLE}_enabled_next_run_at + ON {self._qualified_table(self.SCHEDULES_TABLE)} (enabled, next_run_at) + """)) + conn.commit() + + self.schema_ready = True + + def _resolve_run_at( + self, + run_at: Optional[datetime] = None, + delay_seconds: Optional[int] = None, + ) -> datetime: + if run_at and delay_seconds is not None: + raise ValueError("Specify either run_at or delay_seconds") + + if delay_seconds is not None: + return datetime.now() + timedelta(seconds=delay_seconds) + + if run_at is None: + return datetime.now() + + return self._normalize_datetime(run_at) + + def _resolve_next_schedule_run_at( + self, + schedule_type: str, + interval_seconds: Optional[int], + daily_time: Optional[str], + start_at: Optional[datetime], + reference: datetime, + ) -> datetime: + normalized_reference = self._normalize_datetime(reference) + normalized_start_at = self._normalize_datetime(start_at) if start_at else None + baseline = normalized_start_at or normalized_reference + + if schedule_type == 'interval': + if not interval_seconds or interval_seconds <= 0: + raise ValueError("interval_seconds must be greater than 0 for interval schedule") + return baseline if baseline > normalized_reference else normalized_reference + timedelta(seconds=interval_seconds) + + if schedule_type == 'daily': + if not daily_time: + raise ValueError("daily_time is required for daily schedule") + parsed_time = self._parse_daily_time(daily_time) + candidate_date = baseline.date() + candidate = datetime.combine(candidate_date, parsed_time) + if normalized_start_at and candidate < normalized_start_at: + candidate = datetime.combine(normalized_start_at.date(), parsed_time) + if candidate <= normalized_reference: + candidate = candidate + timedelta(days=1) + return candidate + + raise ValueError("schedule_type must be one of: interval, daily") + + def _next_schedule_run_from_row(self, row: Dict[str, Any], reference: datetime) -> datetime: + schedule_type = row['schedule_type'] + if schedule_type == 'interval': + return reference + timedelta(seconds=int(row['interval_seconds'])) + if schedule_type == 'daily': + parsed_time = self._parse_daily_time(row['daily_time']) + candidate = datetime.combine(reference.date(), parsed_time) + if candidate <= reference: + candidate += timedelta(days=1) + return candidate + raise ValueError(f"Unsupported schedule_type: {schedule_type}") + + def _parse_daily_time(self, raw_value: str) -> dt_time: + parts = raw_value.split(':') + if len(parts) not in (2, 3): + raise ValueError("daily_time must be HH:MM or HH:MM:SS") + hour = int(parts[0]) + minute = int(parts[1]) + second = int(parts[2]) if len(parts) == 3 else 0 + return dt_time(hour=hour, minute=minute, second=second) + + def _normalize_datetime(self, value: Optional[datetime]) -> Optional[datetime]: + if value is None: + return None + if value.tzinfo is not None: + return value.astimezone().replace(tzinfo=None) + return value + + def _worker_loop(self): + self._ensure_schema() + + while True: + try: + self._materialize_due_schedules() + job_row = self._claim_next_due_job() + if job_row: + self._execute_job(self._job_from_row(job_row)) + continue + except Exception: + time.sleep(1.0) + continue + + with self.condition: + self.condition.wait(timeout=Config.QUEUE_POLL_SECONDS) + + def _materialize_due_schedules(self): + now = datetime.now() + grace_cutoff = now - timedelta(seconds=Config.SCHEDULE_GRACE_SECONDS) + with self._get_engine().connect() as conn: + due_rows = conn.execute(self._text(f""" + SELECT * + FROM {self._qualified_table(self.SCHEDULES_TABLE)} + WHERE enabled = TRUE + AND next_run_at <= :now + ORDER BY next_run_at, created_at + FOR UPDATE + """), {'now': now}).mappings().all() + + for row in due_rows: + missed_run = row['next_run_at'] < grace_cutoff + next_run_at = self._next_schedule_run_from_row(row, now) + job_id = row['last_job_id'] + force_full = bool(row.get('initial_force_full', False)) + + if not missed_run or row['catch_up_missed_runs']: + job_id = str(uuid.uuid4()) + conn.execute(self._text(f""" + INSERT INTO {self._qualified_table(self.JOBS_TABLE)} + (job_id, schedule_id, created_at, run_at, tables_json, send_email, dry_run, read_limit, force_full, status) + VALUES + ( + :job_id, :schedule_id, :created_at, :run_at, CAST(:tables_json AS jsonb), + :send_email, :dry_run, :read_limit, :force_full, 'queued' + ) + """), { + 'job_id': job_id, + 'schedule_id': row['schedule_id'], + 'created_at': now, + 'run_at': now, + 'tables_json': json.dumps(row['tables_json']) if row['tables_json'] is not None else None, + 'send_email': row['send_email'], + 'dry_run': row['dry_run'], + 'read_limit': row['read_limit'], + 'force_full': force_full, + }) + conn.execute(self._text(f""" + UPDATE {self._qualified_table(self.SCHEDULES_TABLE)} + SET updated_at = :updated_at, + last_enqueued_at = :last_enqueued_at, + last_job_id = :last_job_id, + initial_force_full = CASE + WHEN :reset_initial_force_full THEN FALSE + ELSE initial_force_full + END, + next_run_at = :next_run_at + WHERE schedule_id = :schedule_id + """), { + 'updated_at': now, + 'last_enqueued_at': now if (not missed_run or row['catch_up_missed_runs']) else row['last_enqueued_at'], + 'last_job_id': job_id, + 'reset_initial_force_full': (not missed_run or row['catch_up_missed_runs']) and force_full, + 'next_run_at': next_run_at, + 'schedule_id': row['schedule_id'], + }) + + conn.commit() + + if due_rows: + with self.condition: + self.condition.notify_all() + + def _claim_next_due_job(self) -> Optional[Dict[str, Any]]: + now = datetime.now() + sql = self._text(f""" + WITH next_job AS ( + SELECT job_id + FROM {self._qualified_table(self.JOBS_TABLE)} + WHERE status = 'queued' + AND run_at <= :now + ORDER BY run_at, queue_sequence + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + UPDATE {self._qualified_table(self.JOBS_TABLE)} job + SET status = 'running', + started_at = :now, + finished_at = NULL, + error = NULL, + report_json = NULL + FROM next_job + WHERE job.job_id = next_job.job_id + RETURNING job.* + """) + with self._get_engine().connect() as conn: + row = conn.execute(sql, {'now': now}).mappings().first() + conn.commit() + return row + + def _execute_job(self, job: MigrationJob): + migrator = None + try: + from .migrator import DatabaseMigrator + + config = Config() + migrator = DatabaseMigrator(config) + report = migrator.run_migration( + table_names=job.tables, + send_email=False, + dry_run=job.dry_run, + read_limit=job.read_limit, + force_full=job.force_full, + ) + migrator.cleanup_old_logs(days_to_keep=7) + self._finish_job(job.job_id, status='completed', report=report, error=None) + if job.send_email: + migrator.send_notification(report) + except Exception as exc: + self._finish_job(job.job_id, status='failed', report=None, error=str(exc)) + if job.send_email and migrator is not None: + try: + migrator.send_failure_notification( + error=str(exc), + table_names=job.tables, + job_id=job.job_id, + ) + except Exception: + pass + + def _finish_job( + self, + job_id: str, + status: str, + report: Optional[Dict[str, Any]], + error: Optional[str], + ): + with self._get_engine().connect() as conn: + conn.execute(self._text(f""" + UPDATE {self._qualified_table(self.JOBS_TABLE)} + SET status = :status, + finished_at = :finished_at, + report_json = CAST(:report_json AS jsonb), + error = :error + WHERE job_id = :job_id + """), { + 'status': status, + 'finished_at': datetime.now(), + 'report_json': json.dumps(report) if report is not None else None, + 'error': error, + 'job_id': job_id, + }) + conn.commit() + + def _job_from_row(self, row: Optional[Dict[str, Any]]) -> Optional[MigrationJob]: + if row is None: + return None + + tables = row['tables_json'] + if isinstance(tables, str): + tables = json.loads(tables) + report = row['report_json'] + if isinstance(report, str): + report = json.loads(report) + + return MigrationJob( + job_id=row['job_id'], + created_at=row['created_at'], + run_at=row['run_at'], + tables=tables, + send_email=row['send_email'], + dry_run=row['dry_run'], + read_limit=row['read_limit'], + force_full=row.get('force_full', False), + status=row['status'], + started_at=row['started_at'], + finished_at=row['finished_at'], + report=report, + error=row['error'], + queue_sequence=row['queue_sequence'], + schedule_id=row['schedule_id'], + ) + + def _schedule_from_row(self, row: Optional[Dict[str, Any]]) -> Optional[MigrationSchedule]: + if row is None: + return None + + tables = row['tables_json'] + if isinstance(tables, str): + tables = json.loads(tables) + + return MigrationSchedule( + schedule_id=row['schedule_id'], + created_at=row['created_at'], + updated_at=row['updated_at'], + name=row['name'], + schedule_type=row['schedule_type'], + enabled=row['enabled'], + catch_up_missed_runs=row['catch_up_missed_runs'], + initial_force_full=row.get('initial_force_full', False), + tables=tables, + send_email=row['send_email'], + dry_run=row['dry_run'], + read_limit=row['read_limit'], + interval_seconds=row['interval_seconds'], + daily_time=row['daily_time'], + start_at=row['start_at'], + next_run_at=row['next_run_at'], + last_enqueued_at=row['last_enqueued_at'], + last_job_id=row['last_job_id'], + ) + + +migration_queue = MigrationJobQueue() diff --git a/app/table_config_repository.py b/app/table_config_repository.py new file mode 100644 index 0000000..550f3f2 --- /dev/null +++ b/app/table_config_repository.py @@ -0,0 +1,170 @@ +import json +from dataclasses import asdict +from typing import List, Optional + +from .config import Config, TableMigrationConfig + + +class TableConfigRepository: + """Хранилище конфигурации таблиц репликатора в PostgreSQL.""" + + def __init__(self, config: Config, engine): + self.config = config + self.engine = engine + + def _text(self, sql: str): + from sqlalchemy import text + + return text(sql) + + def _quote_identifier(self, identifier: str) -> str: + return '"' + identifier.replace('"', '""') + '"' + + def _qualified_table(self) -> str: + return ( + f'{self._quote_identifier(self.config.REPLICATOR_SCHEMA)}.' + f'{self._quote_identifier(self.config.TABLE_CONFIGS_TABLE)}' + ) + + def ensure_table(self): + """Создание таблицы конфигурации в служебной схеме.""" + with self.engine.connect() as conn: + conn.execute(self._text( + f'CREATE SCHEMA IF NOT EXISTS {self._quote_identifier(self.config.REPLICATOR_SCHEMA)}' + )) + conn.execute(self._text(f""" + CREATE TABLE IF NOT EXISTS {self._qualified_table()} ( + source_table text PRIMARY KEY, + target_table text NULL, + mode text NOT NULL, + initial_load_mode text NOT NULL, + life_table text NULL, + datetime_column text NOT NULL, + sequence_column text NULL, + order_columns_json jsonb NOT NULL DEFAULT '[]'::jsonb, + operation_column text NULL, + delete_operations_json jsonb NOT NULL DEFAULT '["d"]'::jsonb, + upsert_operations_json jsonb NOT NULL DEFAULT '["i","u"]'::jsonb, + primary_key_json jsonb NOT NULL DEFAULT '[]'::jsonb, + exclude_columns_json jsonb NOT NULL DEFAULT '[]'::jsonb, + timescale boolean NOT NULL DEFAULT FALSE, + timescale_time_column text NULL, + enabled boolean NOT NULL DEFAULT TRUE, + created_at timestamp NOT NULL DEFAULT now(), + updated_at timestamp NOT NULL DEFAULT now() + ) + """)) + conn.execute(self._text(f""" + CREATE INDEX IF NOT EXISTS idx_{self.config.TABLE_CONFIGS_TABLE}_enabled + ON {self._qualified_table()} (enabled, source_table) + """)) + conn.commit() + + def seed_defaults_if_empty(self): + """Первичная загрузка конфигурации из bootstrap defaults.""" + self.ensure_table() + with self.engine.connect() as conn: + count = conn.execute(self._text( + f'SELECT COUNT(*) FROM {self._qualified_table()}' + )).scalar() + if count and int(count) > 0: + return + + for table_config in self.config.DEFAULT_TABLE_MIGRATIONS: + payload = asdict(table_config) + conn.execute(self._text(f""" + INSERT INTO {self._qualified_table()} + ( + source_table, target_table, mode, initial_load_mode, life_table, + datetime_column, sequence_column, order_columns_json, operation_column, + delete_operations_json, upsert_operations_json, primary_key_json, + exclude_columns_json, timescale, timescale_time_column, enabled + ) + VALUES + ( + :source_table, :target_table, :mode, :initial_load_mode, :life_table, + :datetime_column, :sequence_column, CAST(:order_columns_json AS jsonb), :operation_column, + CAST(:delete_operations_json AS jsonb), CAST(:upsert_operations_json AS jsonb), CAST(:primary_key_json AS jsonb), + CAST(:exclude_columns_json AS jsonb), :timescale, :timescale_time_column, :enabled + ) + """), { + 'source_table': payload['source_table'], + 'target_table': payload['target_table'], + 'mode': payload['mode'], + 'initial_load_mode': payload['initial_load_mode'], + 'life_table': payload['life_table'], + 'datetime_column': payload['datetime_column'], + 'sequence_column': payload['sequence_column'], + 'order_columns_json': json.dumps(payload['order_columns']), + 'operation_column': payload['operation_column'], + 'delete_operations_json': json.dumps(payload['delete_operations']), + 'upsert_operations_json': json.dumps(payload['upsert_operations']), + 'primary_key_json': json.dumps(payload['primary_key']), + 'exclude_columns_json': json.dumps(payload['exclude_columns']), + 'timescale': payload['timescale'], + 'timescale_time_column': payload['timescale_time_column'], + 'enabled': payload['enabled'], + }) + conn.commit() + + def load_configs(self, enabled_only: bool = True, seed_defaults: bool = True) -> List[TableMigrationConfig]: + """Чтение конфигурации таблиц из БД.""" + self.ensure_table() + if seed_defaults: + self.seed_defaults_if_empty() + + where_clause = 'WHERE enabled = TRUE' if enabled_only else '' + sql = self._text(f""" + SELECT * + FROM {self._qualified_table()} + {where_clause} + ORDER BY source_table + """) + with self.engine.connect() as conn: + rows = conn.execute(sql).mappings().all() + + return [self._row_to_config(row) for row in rows] + + def get_config(self, table_name: str) -> Optional[TableMigrationConfig]: + """Чтение одной конфигурации по source_table или target_table.""" + self.ensure_table() + sql = self._text(f""" + SELECT * + FROM {self._qualified_table()} + WHERE enabled = TRUE + AND ( + lower(source_table) = :table_name + OR lower(COALESCE(target_table, source_table)) = :table_name + ) + LIMIT 1 + """) + with self.engine.connect() as conn: + row = conn.execute(sql, {'table_name': table_name.lower()}).mappings().first() + return self._row_to_config(row) if row else None + + def _row_to_config(self, row) -> TableMigrationConfig: + return TableMigrationConfig( + source_table=row['source_table'], + target_table=row['target_table'], + mode=row['mode'], + initial_load_mode=row['initial_load_mode'], + life_table=row['life_table'], + datetime_column=row['datetime_column'], + sequence_column=row['sequence_column'], + order_columns=self._json_field(row['order_columns_json']), + operation_column=row['operation_column'], + delete_operations=self._json_field(row['delete_operations_json']), + upsert_operations=self._json_field(row['upsert_operations_json']), + primary_key=self._json_field(row['primary_key_json']), + exclude_columns=self._json_field(row['exclude_columns_json']), + timescale=row['timescale'], + timescale_time_column=row['timescale_time_column'], + enabled=row['enabled'], + ) + + def _json_field(self, value) -> List[str]: + if value is None: + return [] + if isinstance(value, str): + return json.loads(value) + return list(value) diff --git a/app/worker.py b/app/worker.py new file mode 100644 index 0000000..3cfde99 --- /dev/null +++ b/app/worker.py @@ -0,0 +1,26 @@ +import signal +import time + +from .config import Config +from .queue import migration_queue + + +def main(): + """Запуск отдельного worker-процесса очереди и расписаний.""" + stop_requested = False + + def request_stop(_signum, _frame): + nonlocal stop_requested + stop_requested = True + + signal.signal(signal.SIGINT, request_stop) + signal.signal(signal.SIGTERM, request_stop) + + migration_queue.start() + + while not stop_requested: + time.sleep(max(Config.QUEUE_POLL_SECONDS, 1.0)) + + +if __name__ == "__main__": + main() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..33d0678 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + api: + build: . + container_name: syncio-api + restart: unless-stopped + env_file: + - .env + environment: + START_API_WORKER: "false" + TZ: "${TZ:-Asia/Yakutsk}" + ports: + - "8000:8000" + volumes: + - ./logs:/app/logs + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + + worker: + build: . + container_name: syncio-worker + restart: unless-stopped + env_file: + - .env + environment: + START_API_WORKER: "false" + TZ: "${TZ:-Asia/Yakutsk}" + volumes: + - ./logs:/app/logs + command: ["python", "-m", "app.worker"] diff --git a/errors_in_server_docker.log b/errors_in_server_docker.log new file mode 100644 index 0000000..38e61b6 --- /dev/null +++ b/errors_in_server_docker.log @@ -0,0 +1 @@ +{"errors": [{"message": "Ошибка при создании внешнего ключа fk_hlt_disp_card_rf_CitizenCategoryID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_citizencategory\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_card\" \n ADD CONSTRAINT \"fk_hlt_disp_card_rf_CitizenCategoryID\" \n FOREIGN KEY (\"rf_CitizenCategoryID\") \n REFERENCES \"oms_citizencategory\" (\"CitizenCategoryID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:03:13.750908", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_citizencategory\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_citizencategory\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_card\" \n ADD CONSTRAINT \"fk_hlt_disp_card_rf_CitizenCategoryID\" \n FOREIGN KEY (\"rf_CitizenCategoryID\") \n REFERENCES \"oms_citizencategory\" (\"CitizenCategoryID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_disp_card_rf_DogovorPatientID", "exception": "(psycopg2.errors.UndefinedTable) relation \"hlt_dogovorpatient\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_card\" \n ADD CONSTRAINT \"fk_hlt_disp_card_rf_DogovorPatientID\" \n FOREIGN KEY (\"rf_DogovorPatientID\") \n REFERENCES \"hlt_dogovorpatient\" (\"DogovorPatientID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:03:13.752966", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"hlt_dogovorpatient\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"hlt_dogovorpatient\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_card\" \n ADD CONSTRAINT \"fk_hlt_disp_card_rf_DogovorPatientID\" \n FOREIGN KEY (\"rf_DogovorPatientID\") \n REFERENCES \"hlt_dogovorpatient\" (\"DogovorPatientID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_disp_patientmodel_rf_kl_OrphanAgeGroupID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_orphanagegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_patientmodel\" \n ADD CONSTRAINT \"fk_hlt_disp_patientmodel_rf_kl_OrphanAgeGroupID\" \n FOREIGN KEY (\"rf_kl_OrphanAgeGroupID\") \n REFERENCES \"oms_kl_orphanagegroup\" (\"kl_OrphanAgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:17:28.835112", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_orphanagegroup\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_orphanagegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_disp_patientmodel\" \n ADD CONSTRAINT \"fk_hlt_disp_patientmodel_rf_kl_OrphanAgeGroupID\" \n FOREIGN KEY (\"rf_kl_OrphanAgeGroupID\") \n REFERENCES \"oms_kl_orphanagegroup\" (\"kl_OrphanAgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_docprvd_rf_kl_SubComissionTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_subcomissiontype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_docprvd\" \n ADD CONSTRAINT \"fk_hlt_docprvd_rf_kl_SubComissionTypeID\" \n FOREIGN KEY (\"rf_kl_SubComissionTypeID\") \n REFERENCES \"oms_kl_subcomissiontype\" (\"kl_SubComissionTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:04.153710", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_subcomissiontype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_subcomissiontype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_docprvd\" \n ADD CONSTRAINT \"fk_hlt_docprvd_rf_kl_SubComissionTypeID\" \n FOREIGN KEY (\"rf_kl_SubComissionTypeID\") \n REFERENCES \"oms_kl_subcomissiontype\" (\"kl_SubComissionTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_docprvd_rf_KV_KATID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kv_kat\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_docprvd\" \n ADD CONSTRAINT \"fk_hlt_docprvd_rf_KV_KATID\" \n FOREIGN KEY (\"rf_KV_KATID\") \n REFERENCES \"oms_kv_kat\" (\"KV_KATID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:04.155836", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kv_kat\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kv_kat\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_docprvd\" \n ADD CONSTRAINT \"fk_hlt_docprvd_rf_KV_KATID\" \n FOREIGN KEY (\"rf_KV_KATID\") \n REFERENCES \"oms_kv_kat\" (\"KV_KATID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_invoice_rf_InvoiceTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"hlt_invoicetype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_invoice\" \n ADD CONSTRAINT \"fk_hlt_invoice_rf_InvoiceTypeID\" \n FOREIGN KEY (\"rf_InvoiceTypeID\") \n REFERENCES \"hlt_invoicetype\" (\"InvoiceTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:40.611170", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"hlt_invoicetype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"hlt_invoicetype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_invoice\" \n ADD CONSTRAINT \"fk_hlt_invoice_rf_InvoiceTypeID\" \n FOREIGN KEY (\"rf_InvoiceTypeID\") \n REFERENCES \"hlt_invoicetype\" (\"InvoiceTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_invoice_rf_PaymentTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"hlt_paymenttype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_invoice\" \n ADD CONSTRAINT \"fk_hlt_invoice_rf_PaymentTypeID\" \n FOREIGN KEY (\"rf_PaymentTypeID\") \n REFERENCES \"hlt_paymenttype\" (\"PaymentTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:40.613150", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"hlt_paymenttype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"hlt_paymenttype\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_invoice\" \n ADD CONSTRAINT \"fk_hlt_invoice_rf_PaymentTypeID\" \n FOREIGN KEY (\"rf_PaymentTypeID\") \n REFERENCES \"hlt_paymenttype\" (\"PaymentTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_lpudoctor_rf_kl_SexID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_sex\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_lpudoctor\" \n ADD CONSTRAINT \"fk_hlt_lpudoctor_rf_kl_SexID\" \n FOREIGN KEY (\"rf_kl_SexID\") \n REFERENCES \"oms_kl_sex\" (\"kl_SexID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:44.900986", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_sex\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_sex\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_lpudoctor\" \n ADD CONSTRAINT \"fk_hlt_lpudoctor_rf_kl_SexID\" \n FOREIGN KEY (\"rf_kl_SexID\") \n REFERENCES \"oms_kl_sex\" (\"kl_SexID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_lpudoctor_rf_TypeDocID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_typedoc\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_lpudoctor\" \n ADD CONSTRAINT \"fk_hlt_lpudoctor_rf_TypeDocID\" \n FOREIGN KEY (\"rf_TypeDocID\") \n REFERENCES \"oms_typedoc\" (\"TYPEDOCID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T00:18:44.902873", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_typedoc\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_typedoc\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_lpudoctor\" \n ADD CONSTRAINT \"fk_hlt_lpudoctor_rf_TypeDocID\" \n FOREIGN KEY (\"rf_TypeDocID\") \n REFERENCES \"oms_typedoc\" (\"TYPEDOCID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_mkb_tap_rf_kl_DiagnosisCredibilityID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_diagnosiscredibility\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_mkb_tap\" \n ADD CONSTRAINT \"fk_hlt_mkb_tap_rf_kl_DiagnosisCredibilityID\" \n FOREIGN KEY (\"rf_kl_DiagnosisCredibilityID\") \n REFERENCES \"oms_kl_diagnosiscredibility\" (\"kl_DiagnosisCredibilityID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T01:04:58.681267", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_diagnosiscredibility\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_diagnosiscredibility\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_mkb_tap\" \n ADD CONSTRAINT \"fk_hlt_mkb_tap_rf_kl_DiagnosisCredibilityID\" \n FOREIGN KEY (\"rf_kl_DiagnosisCredibilityID\") \n REFERENCES \"oms_kl_diagnosiscredibility\" (\"kl_DiagnosisCredibilityID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_mkb_tap_rf_kl_DiagnosisValidityID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_diagnosisvalidity\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_mkb_tap\" \n ADD CONSTRAINT \"fk_hlt_mkb_tap_rf_kl_DiagnosisValidityID\" \n FOREIGN KEY (\"rf_kl_DiagnosisValidityID\") \n REFERENCES \"oms_kl_diagnosisvalidity\" (\"kl_DiagnosisValidityID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T01:04:58.683315", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_diagnosisvalidity\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_diagnosisvalidity\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_mkb_tap\" \n ADD CONSTRAINT \"fk_hlt_mkb_tap_rf_kl_DiagnosisValidityID\" \n FOREIGN KEY (\"rf_kl_DiagnosisValidityID\") \n REFERENCES \"oms_kl_diagnosisvalidity\" (\"kl_DiagnosisValidityID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при выполнении VACUUM ANALYZE для таблицы hlt_mkb_tap", "exception": "(psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_mkb_tap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)", "timestamp": "2026-04-16T01:04:58.685529", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ActiveSqlTransaction: VACUUM cannot run inside a transaction block\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 871, in vacuum_analyze_table\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.InternalError: (psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_mkb_tap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)\n"}, {"message": "Ошибка при выполнении VACUUM ANALYZE для таблицы hlt_reestridcase", "exception": "(psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_reestridcase\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)", "timestamp": "2026-04-16T01:18:44.293369", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ActiveSqlTransaction: VACUUM cannot run inside a transaction block\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 871, in vacuum_analyze_table\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.InternalError: (psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_reestridcase\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_regmedicalcheck_rf_kl_OncoSocialGroupID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_oncosocialgroup\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_regmedicalcheck\" \n ADD CONSTRAINT \"fk_hlt_regmedicalcheck_rf_kl_OncoSocialGroupID\" \n FOREIGN KEY (\"rf_kl_OncoSocialGroupID\") \n REFERENCES \"oms_kl_oncosocialgroup\" (\"kl_OncoSocialGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T01:28:31.283306", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_oncosocialgroup\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_oncosocialgroup\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_regmedicalcheck\" \n ADD CONSTRAINT \"fk_hlt_regmedicalcheck_rf_kl_OncoSocialGroupID\" \n FOREIGN KEY (\"rf_kl_OncoSocialGroupID\") \n REFERENCES \"oms_kl_oncosocialgroup\" (\"kl_OncoSocialGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании индекса idx_hlt_smtap_rf_omsservicemedicalid__smtapid__x_edition__x_status", "exception": "(psycopg2.errors.TooManyColumns) cannot use more than 32 columns in an index\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_hlt_smtap_rf_omsservicemedicalid__smtapid__x_edition__x_status\" ON \"hlt_smtap\" (\"SMTAPID\", \"x_Edition\", \"x_Status\", \"rf_TAPID\", \"REG_S\", \"SMTAPGuid\", \"IsFake\", \"rf_LPUDoctorID\", \"Count\", \"DATE_P\", \"rf_DoctorVisitTableID\", \"FLAGS\", \"rf_LPUID\", \"rf_MKBID\", \"Description\", \"rf_TariffID\", \"rf_DepartmentID\", \"CreateUserName\", \"EditUserName\", \"FlagBill\", \"FlagComplete\", \"FlagPay\", \"FlagStatist\", \"rf_CreateUserID\", \"rf_EditUserID\", \"rf_InvoiceID\", \"rf_DocPRVDID\", \"Sum_Opl\", \"Sum_V\", \"DATE_E\", \"rf_kl_VisitPlaceID\", \"rf_LPUDoctor_SID\", \"rf_BillServiceID\", \"rf_RootSMTAPID\", \"rf_OperationID\", \"rf_kl_TeethID\", \"rf_kl_ActionTeethID\", \"rf_usl_ProfitTypeID\", \"rf_omsServiceMedicalID\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)", "timestamp": "2026-04-16T02:00:34.922886", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.TooManyColumns: cannot use more than 32 columns in an index\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 710, in create_pg_indexes\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.OperationalError: (psycopg2.errors.TooManyColumns) cannot use more than 32 columns in an index\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_hlt_smtap_rf_omsservicemedicalid__smtapid__x_edition__x_status\" ON \"hlt_smtap\" (\"SMTAPID\", \"x_Edition\", \"x_Status\", \"rf_TAPID\", \"REG_S\", \"SMTAPGuid\", \"IsFake\", \"rf_LPUDoctorID\", \"Count\", \"DATE_P\", \"rf_DoctorVisitTableID\", \"FLAGS\", \"rf_LPUID\", \"rf_MKBID\", \"Description\", \"rf_TariffID\", \"rf_DepartmentID\", \"CreateUserName\", \"EditUserName\", \"FlagBill\", \"FlagComplete\", \"FlagPay\", \"FlagStatist\", \"rf_CreateUserID\", \"rf_EditUserID\", \"rf_InvoiceID\", \"rf_DocPRVDID\", \"Sum_Opl\", \"Sum_V\", \"DATE_E\", \"rf_kl_VisitPlaceID\", \"rf_LPUDoctor_SID\", \"rf_BillServiceID\", \"rf_RootSMTAPID\", \"rf_OperationID\", \"rf_kl_TeethID\", \"rf_kl_ActionTeethID\", \"rf_usl_ProfitTypeID\", \"rf_omsServiceMedicalID\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_smtap_rf_DogovorPayingID", "exception": "(psycopg2.errors.UndefinedTable) relation \"hlt_dogovorpaying\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_smtap\" \n ADD CONSTRAINT \"fk_hlt_smtap_rf_DogovorPayingID\" \n FOREIGN KEY (\"rf_DogovorPayingID\") \n REFERENCES \"hlt_dogovorpaying\" (\"DogovorPayingID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T02:00:41.569485", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"hlt_dogovorpaying\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"hlt_dogovorpaying\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_smtap\" \n ADD CONSTRAINT \"fk_hlt_smtap_rf_DogovorPayingID\" \n FOREIGN KEY (\"rf_DogovorPayingID\") \n REFERENCES \"hlt_dogovorpaying\" (\"DogovorPayingID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при выполнении VACUUM ANALYZE для таблицы hlt_smtap", "exception": "(psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_smtap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)", "timestamp": "2026-04-16T02:00:41.571359", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ActiveSqlTransaction: VACUUM cannot run inside a transaction block\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 871, in vacuum_analyze_table\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.InternalError: (psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_smtap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_tap_rf_FomsRegMedicalCheckStatusID", "exception": "(psycopg2.errors.UndefinedTable) relation \"hlt_fomsregmedicalcheckstatus\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_tap\" \n ADD CONSTRAINT \"fk_hlt_tap_rf_FomsRegMedicalCheckStatusID\" \n FOREIGN KEY (\"rf_FomsRegMedicalCheckStatusID\") \n REFERENCES \"hlt_fomsregmedicalcheckstatus\" (\"FomsRegMedicalCheckStatusID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:24.249339", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"hlt_fomsregmedicalcheckstatus\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"hlt_fomsregmedicalcheckstatus\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_tap\" \n ADD CONSTRAINT \"fk_hlt_tap_rf_FomsRegMedicalCheckStatusID\" \n FOREIGN KEY (\"rf_FomsRegMedicalCheckStatusID\") \n REFERENCES \"hlt_fomsregmedicalcheckstatus\" (\"FomsRegMedicalCheckStatusID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_hlt_tap_rf_StatusGisOmsID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_statusgisoms\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_tap\" \n ADD CONSTRAINT \"fk_hlt_tap_rf_StatusGisOmsID\" \n FOREIGN KEY (\"rf_StatusGisOmsID\") \n REFERENCES \"oms_statusgisoms\" (\"StatusGisOmsID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:24.462757", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_statusgisoms\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_statusgisoms\" does not exist\n\n[SQL: \n ALTER TABLE \"hlt_tap\" \n ADD CONSTRAINT \"fk_hlt_tap_rf_StatusGisOmsID\" \n FOREIGN KEY (\"rf_StatusGisOmsID\") \n REFERENCES \"oms_statusgisoms\" (\"StatusGisOmsID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при выполнении VACUUM ANALYZE для таблицы hlt_tap", "exception": "(psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_tap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)", "timestamp": "2026-04-16T03:10:24.464868", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ActiveSqlTransaction: VACUUM cannot run inside a transaction block\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 871, in vacuum_analyze_table\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.InternalError: (psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"hlt_tap\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_AddressID", "exception": "(psycopg2.errors.UndefinedTable) relation \"kla_address\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_AddressID\" \n FOREIGN KEY (\"rf_AddressID\") \n REFERENCES \"kla_address\" (\"AddressID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.836713", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"kla_address\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"kla_address\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_AddressID\" \n FOREIGN KEY (\"rf_AddressID\") \n REFERENCES \"kla_address\" (\"AddressID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_AgeGroupID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_agegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_AgeGroupID\" \n FOREIGN KEY (\"rf_kl_AgeGroupID\") \n REFERENCES \"oms_kl_agegroup\" (\"kl_AgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.838673", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_agegroup\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_agegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_AgeGroupID\" \n FOREIGN KEY (\"rf_kl_AgeGroupID\") \n REFERENCES \"oms_kl_agegroup\" (\"kl_AgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_CategoryID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_category\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_CategoryID\" \n FOREIGN KEY (\"rf_kl_CategoryID\") \n REFERENCES \"oms_kl_category\" (\"kl_CategoryID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.840597", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_category\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_category\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_CategoryID\" \n FOREIGN KEY (\"rf_kl_CategoryID\") \n REFERENCES \"oms_kl_category\" (\"kl_CategoryID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_LpuMedTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_lpumedtype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_LpuMedTypeID\" \n FOREIGN KEY (\"rf_kl_LpuMedTypeID\") \n REFERENCES \"oms_kl_lpumedtype\" (\"kl_LpuMedTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.842477", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_lpumedtype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_lpumedtype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_LpuMedTypeID\" \n FOREIGN KEY (\"rf_kl_LpuMedTypeID\") \n REFERENCES \"oms_kl_lpumedtype\" (\"kl_LpuMedTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_MedCareTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_medcaretype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_MedCareTypeID\" \n FOREIGN KEY (\"rf_kl_MedCareTypeID\") \n REFERENCES \"oms_kl_medcaretype\" (\"kl_MedCareTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.844486", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_medcaretype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_medcaretype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_MedCareTypeID\" \n FOREIGN KEY (\"rf_kl_MedCareTypeID\") \n REFERENCES \"oms_kl_medcaretype\" (\"kl_MedCareTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_TariffTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_tarifftype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_TariffTypeID\" \n FOREIGN KEY (\"rf_kl_TariffTypeID\") \n REFERENCES \"oms_kl_tarifftype\" (\"kl_TariffTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.846319", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_tarifftype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_tarifftype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_TariffTypeID\" \n FOREIGN KEY (\"rf_kl_TariffTypeID\") \n REFERENCES \"oms_kl_tarifftype\" (\"kl_TariffTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_kl_Type_LPUID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_type_lpu\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_Type_LPUID\" \n FOREIGN KEY (\"rf_kl_Type_LPUID\") \n REFERENCES \"oms_kl_type_lpu\" (\"kl_Type_LPUID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.848564", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_type_lpu\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_type_lpu\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_kl_Type_LPUID\" \n FOREIGN KEY (\"rf_kl_Type_LPUID\") \n REFERENCES \"oms_kl_type_lpu\" (\"kl_Type_LPUID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_OKATOID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_okato\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_OKATOID\" \n FOREIGN KEY (\"rf_OKATOID\") \n REFERENCES \"oms_okato\" (\"OKATOID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.855167", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_okato\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_okato\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_OKATOID\" \n FOREIGN KEY (\"rf_OKATOID\") \n REFERENCES \"oms_okato\" (\"OKATOID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_OrganisationID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_organisation\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_OrganisationID\" \n FOREIGN KEY (\"rf_OrganisationID\") \n REFERENCES \"oms_organisation\" (\"OrganisationID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.857247", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_organisation\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_organisation\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_OrganisationID\" \n FOREIGN KEY (\"rf_OrganisationID\") \n REFERENCES \"oms_organisation\" (\"OrganisationID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_lpu_rf_TariffTargetID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_tarifftarget\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_TariffTargetID\" \n FOREIGN KEY (\"rf_TariffTargetID\") \n REFERENCES \"oms_tarifftarget\" (\"TariffTargetID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T03:10:42.865005", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_tarifftarget\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_tarifftarget\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_lpu\" \n ADD CONSTRAINT \"fk_oms_lpu_rf_TariffTargetID\" \n FOREIGN KEY (\"rf_TariffTargetID\") \n REFERENCES \"oms_tarifftarget\" (\"TariffTargetID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании индекса idx_oms_paramvalue_idx_pramavalue_datevaluerf_paramid", "exception": "(psycopg2.errors.ProgramLimitExceeded) index row requires 10128 bytes, maximum size is 8191\nCONTEXT: parallel worker\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_oms_paramvalue_idx_pramavalue_datevaluerf_paramid\" ON \"oms_paramvalue\" (\"Date\", \"Value\", \"rf_ParamID\", \"PatientGUID\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)", "timestamp": "2026-04-16T07:51:25.356176", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ProgramLimitExceeded: index row requires 10128 bytes, maximum size is 8191\nCONTEXT: parallel worker\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 710, in create_pg_indexes\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.OperationalError: (psycopg2.errors.ProgramLimitExceeded) index row requires 10128 bytes, maximum size is 8191\nCONTEXT: parallel worker\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_oms_paramvalue_idx_pramavalue_datevaluerf_paramid\" ON \"oms_paramvalue\" (\"Date\", \"Value\", \"rf_ParamID\", \"PatientGUID\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_paramvalue_rf_mn_DocLPUID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_mn_doclpu\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_mn_DocLPUID\" \n FOREIGN KEY (\"rf_mn_DocLPUID\") \n REFERENCES \"oms_mn_doclpu\" (\"mn_DocLPUID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:52:14.115262", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_mn_doclpu\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_mn_doclpu\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_mn_DocLPUID\" \n FOREIGN KEY (\"rf_mn_DocLPUID\") \n REFERENCES \"oms_mn_doclpu\" (\"mn_DocLPUID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_paramvalue_rf_mn_PersonID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_mn_person\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_mn_PersonID\" \n FOREIGN KEY (\"rf_mn_PersonID\") \n REFERENCES \"oms_mn_person\" (\"mn_PersonID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:52:14.117917", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_mn_person\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_mn_person\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_mn_PersonID\" \n FOREIGN KEY (\"rf_mn_PersonID\") \n REFERENCES \"oms_mn_person\" (\"mn_PersonID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_paramvalue_rf_ParamID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_param\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_ParamID\" \n FOREIGN KEY (\"rf_ParamID\") \n REFERENCES \"oms_param\" (\"ParamID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:52:14.121537", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_param\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_param\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_ParamID\" \n FOREIGN KEY (\"rf_ParamID\") \n REFERENCES \"oms_param\" (\"ParamID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_paramvalue_rf_ParamVarID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_paramvar\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_ParamVarID\" \n FOREIGN KEY (\"rf_ParamVarID\") \n REFERENCES \"oms_paramvar\" (\"ParamVarID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:52:14.123917", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_paramvar\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_paramvar\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_paramvalue\" \n ADD CONSTRAINT \"fk_oms_paramvalue_rf_ParamVarID\" \n FOREIGN KEY (\"rf_ParamVarID\") \n REFERENCES \"oms_paramvar\" (\"ParamVarID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при выполнении VACUUM ANALYZE для таблицы oms_paramvalue", "exception": "(psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"oms_paramvalue\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)", "timestamp": "2026-04-16T07:52:14.126357", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ActiveSqlTransaction: VACUUM cannot run inside a transaction block\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 871, in vacuum_analyze_table\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.InternalError: (psycopg2.errors.ActiveSqlTransaction) VACUUM cannot run inside a transaction block\n\n[SQL: VACUUM ANALYZE \"oms_paramvalue\"]\n(Background on this error at: https://sqlalche.me/e/20/2j85)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_prvd_rf_kl_ProfitTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_profittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_prvd\" \n ADD CONSTRAINT \"fk_oms_prvd_rf_kl_ProfitTypeID\" \n FOREIGN KEY (\"rf_kl_ProfitTypeID\") \n REFERENCES \"oms_kl_profittype\" (\"kl_ProfitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:52:18.716558", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_profittype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_profittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_prvd\" \n ADD CONSTRAINT \"fk_oms_prvd_rf_kl_ProfitTypeID\" \n FOREIGN KEY (\"rf_kl_ProfitTypeID\") \n REFERENCES \"oms_kl_profittype\" (\"kl_ProfitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_ActionTeethID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_actionteeth\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_ActionTeethID\" \n FOREIGN KEY (\"rf_kl_ActionTeethID\") \n REFERENCES \"oms_kl_actionteeth\" (\"kl_ActionTeethID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.891610", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_actionteeth\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_actionteeth\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_ActionTeethID\" \n FOREIGN KEY (\"rf_kl_ActionTeethID\") \n REFERENCES \"oms_kl_actionteeth\" (\"kl_ActionTeethID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_AgeGroupID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_agegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_AgeGroupID\" \n FOREIGN KEY (\"rf_kl_AgeGroupID\") \n REFERENCES \"oms_kl_agegroup\" (\"kl_AgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.893773", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_agegroup\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_agegroup\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_AgeGroupID\" \n FOREIGN KEY (\"rf_kl_AgeGroupID\") \n REFERENCES \"oms_kl_agegroup\" (\"kl_AgeGroupID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_DepartmentProfileID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_departmentprofile\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_DepartmentProfileID\" \n FOREIGN KEY (\"rf_kl_DepartmentProfileID\") \n REFERENCES \"oms_kl_departmentprofile\" (\"kl_DepartmentProfileID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.909094", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_departmentprofile\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_departmentprofile\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_DepartmentProfileID\" \n FOREIGN KEY (\"rf_kl_DepartmentProfileID\") \n REFERENCES \"oms_kl_departmentprofile\" (\"kl_DepartmentProfileID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_DepartmentTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_departmenttype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_DepartmentTypeID\" \n FOREIGN KEY (\"rf_kl_DepartmentTypeID\") \n REFERENCES \"oms_kl_departmenttype\" (\"kl_DepartmentTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.911260", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_departmenttype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_departmenttype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_DepartmentTypeID\" \n FOREIGN KEY (\"rf_kl_DepartmentTypeID\") \n REFERENCES \"oms_kl_departmenttype\" (\"kl_DepartmentTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_MedCareLicenceID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_medcarelicence\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareLicenceID\" \n FOREIGN KEY (\"rf_kl_MedCareLicenceID\") \n REFERENCES \"oms_kl_medcarelicence\" (\"kl_MedCareLicenceID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.913090", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_medcarelicence\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_medcarelicence\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareLicenceID\" \n FOREIGN KEY (\"rf_kl_MedCareLicenceID\") \n REFERENCES \"oms_kl_medcarelicence\" (\"kl_MedCareLicenceID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_MedCareTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_medcaretype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareTypeID\" \n FOREIGN KEY (\"rf_kl_MedCareTypeID\") \n REFERENCES \"oms_kl_medcaretype\" (\"kl_MedCareTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.914992", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_medcaretype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_medcaretype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareTypeID\" \n FOREIGN KEY (\"rf_kl_MedCareTypeID\") \n REFERENCES \"oms_kl_medcaretype\" (\"kl_MedCareTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_MedCareUnitID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_medcareunit\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareUnitID\" \n FOREIGN KEY (\"rf_kl_MedCareUnitID\") \n REFERENCES \"oms_kl_medcareunit\" (\"kl_MedCareUnitID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.916851", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_medcareunit\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_medcareunit\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MedCareUnitID\" \n FOREIGN KEY (\"rf_kl_MedCareUnitID\") \n REFERENCES \"oms_kl_medcareunit\" (\"kl_MedCareUnitID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_MetodHMPID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_metodhmp\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MetodHMPID\" \n FOREIGN KEY (\"rf_kl_MetodHMPID\") \n REFERENCES \"oms_kl_metodhmp\" (\"kl_MetodHMPID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.918876", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_metodhmp\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_metodhmp\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_MetodHMPID\" \n FOREIGN KEY (\"rf_kl_MetodHMPID\") \n REFERENCES \"oms_kl_metodhmp\" (\"kl_MetodHMPID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_NomServiceID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_nomservice\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_NomServiceID\" \n FOREIGN KEY (\"rf_kl_NomServiceID\") \n REFERENCES \"oms_kl_nomservice\" (\"kl_NomServiceID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.920623", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_nomservice\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_nomservice\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_NomServiceID\" \n FOREIGN KEY (\"rf_kl_NomServiceID\") \n REFERENCES \"oms_kl_nomservice\" (\"kl_NomServiceID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_OperationTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_operationtype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_OperationTypeID\" \n FOREIGN KEY (\"rf_kl_OperationTypeID\") \n REFERENCES \"oms_kl_operationtype\" (\"kl_OperationTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.922569", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_operationtype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_operationtype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_OperationTypeID\" \n FOREIGN KEY (\"rf_kl_OperationTypeID\") \n REFERENCES \"oms_kl_operationtype\" (\"kl_OperationTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_ProfitTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_profittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_ProfitTypeID\" \n FOREIGN KEY (\"rf_kl_ProfitTypeID\") \n REFERENCES \"oms_kl_profittype\" (\"kl_ProfitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.924434", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_profittype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_profittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_ProfitTypeID\" \n FOREIGN KEY (\"rf_kl_ProfitTypeID\") \n REFERENCES \"oms_kl_profittype\" (\"kl_ProfitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_SexID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_sex\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_SexID\" \n FOREIGN KEY (\"rf_kl_SexID\") \n REFERENCES \"oms_kl_sex\" (\"kl_SexID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.926144", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_sex\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_sex\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_SexID\" \n FOREIGN KEY (\"rf_kl_SexID\") \n REFERENCES \"oms_kl_sex\" (\"kl_SexID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_kl_VisitTypeID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_kl_visittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_VisitTypeID\" \n FOREIGN KEY (\"rf_kl_VisitTypeID\") \n REFERENCES \"oms_kl_visittype\" (\"kl_VisitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.927896", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_kl_visittype\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_kl_visittype\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_kl_VisitTypeID\" \n FOREIGN KEY (\"rf_kl_VisitTypeID\") \n REFERENCES \"oms_kl_visittype\" (\"kl_VisitTypeID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_PRVSID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_prvs\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_PRVSID\" \n FOREIGN KEY (\"rf_PRVSID\") \n REFERENCES \"oms_prvs\" (\"PRVSID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.938385", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_prvs\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_prvs\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_PRVSID\" \n FOREIGN KEY (\"rf_PRVSID\") \n REFERENCES \"oms_prvs\" (\"PRVSID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании внешнего ключа fk_oms_servicemedical_rf_sc_StandartCureID", "exception": "(psycopg2.errors.UndefinedTable) relation \"oms_sc_standartcure\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_sc_StandartCureID\" \n FOREIGN KEY (\"rf_sc_StandartCureID\") \n REFERENCES \"oms_sc_standartcure\" (\"sc_StandartCureID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)", "timestamp": "2026-04-16T07:53:01.940286", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.UndefinedTable: relation \"oms_sc_standartcure\" does not exist\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 843, in create_pg_foreign_keys\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation \"oms_sc_standartcure\" does not exist\n\n[SQL: \n ALTER TABLE \"oms_servicemedical\" \n ADD CONSTRAINT \"fk_oms_servicemedical_rf_sc_StandartCureID\" \n FOREIGN KEY (\"rf_sc_StandartCureID\") \n REFERENCES \"oms_sc_standartcure\" (\"sc_StandartCureID\")\n ]\n(Background on this error at: https://sqlalche.me/e/20/f405)\n"}, {"message": "Ошибка при создании индекса idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid", "exception": "(psycopg2.errors.ProgramLimitExceeded) index row size 2832 exceeds btree version 4 maximum 2704 for index \"idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid\"\nDETAIL: Index row references tuple (27367,11) in relation \"stt_diagnos\".\nHINT: Values larger than 1/3 of a buffer page cannot be indexed.\nConsider a function index of an MD5 hash of the value, or use full text indexing.\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid\" ON \"stt_diagnos\" (\"rf_MKBID\", \"Description\", \"DiagnosID\", \"rf_MigrationPatientID\", \"rf_DiagnosTypeID\", \"Date\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)", "timestamp": "2026-04-16T08:02:13.317979", "traceback": "Traceback (most recent call last):\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\npsycopg2.errors.ProgramLimitExceeded: index row size 2832 exceeds btree version 4 maximum 2704 for index \"idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid\"\nDETAIL: Index row references tuple (27367,11) in relation \"stt_diagnos\".\nHINT: Values larger than 1/3 of a buffer page cannot be indexed.\nConsider a function index of an MD5 hash of the value, or use full text indexing.\n\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/app/app/migrator.py\", line 710, in create_pg_indexes\n conn.execute(text(sql))\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1419, in execute\n return meth(\n ^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/sql/elements.py\", line 527, in _execute_on_connection\n return connection._execute_clauseelement(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1641, in _execute_clauseelement\n ret = self._execute_context(\n ^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1846, in _execute_context\n return self._exec_single_context(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1986, in _exec_single_context\n self._handle_dbapi_exception(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 2363, in _handle_dbapi_exception\n raise sqlalchemy_exception.with_traceback(exc_info[2]) from e\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/base.py\", line 1967, in _exec_single_context\n self.dialect.do_execute(\n File \"/usr/local/lib/python3.12/site-packages/sqlalchemy/engine/default.py\", line 952, in do_execute\n cursor.execute(statement, parameters)\nsqlalchemy.exc.OperationalError: (psycopg2.errors.ProgramLimitExceeded) index row size 2832 exceeds btree version 4 maximum 2704 for index \"idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid\"\nDETAIL: Index row references tuple (27367,11) in relation \"stt_diagnos\".\nHINT: Values larger than 1/3 of a buffer page cannot be indexed.\nConsider a function index of an MD5 hash of the value, or use full text indexing.\n\n[SQL: CREATE INDEX IF NOT EXISTS \"idx_stt_diagnos_ix_rf_migrationpatientid_rf_diagnostypeid\" ON \"stt_diagnos\" (\"rf_MKBID\", \"Description\", \"DiagnosID\", \"rf_MigrationPatientID\", \"rf_DiagnosTypeID\", \"Date\")]\n(Background on this error at: https://sqlalche.me/e/20/e3q8)\n"}], "summary": {"duration": "8:47:38.222537", "end_time": "2026-04-16T08:47:39.407498", "start_time": "2026-04-16T00:00:01.184961", "total_rows": 40198631, "success_rate": 98.0, "total_tables": 50, "failed_tables": 1, "successful_tables": 49}, "failed_tables": [{"name": "hlt_MKAB", "error": "(pymssql.exceptions.OperationalError) (20047, b'DB-Lib error message 20047, severity 9:\\nDBPROCESS is dead or not enabled\\n')\n(Background on this error at: https://sqlalche.me/e/20/e3q8)"}], "successful_tables": [{"name": "die_Card", "rows": 8553}, {"name": "die_Certificate", "rows": 11536}, {"name": "die_Mkb", "rows": 46358}, {"name": "hlt_BillService", "rows": 9858}, {"name": "hlt_dent_tech_OrderSM", "rows": 1}, {"name": "hlt_disp_Card", "rows": 42030}, {"name": "hlt_disp_CardPlan", "rows": 783}, {"name": "hlt_disp_Exam", "rows": 912329}, {"name": "hlt_disp_PatientModel", "rows": 3283}, {"name": "hlt_disp_ServicePM", "rows": 62714}, {"name": "hlt_disp_Type", "rows": 23}, {"name": "hlt_DocPRVD", "rows": 3608}, {"name": "hlt_DUVisit", "rows": 77127}, {"name": "hlt_Invoice", "rows": 9691}, {"name": "hlt_LPUDoctor", "rows": 2154}, {"name": "hlt_MKB_TAP", "rows": 1391150}, {"name": "hlt_PolisMKAB", "rows": 527789}, {"name": "hlt_ReestrIdCase", "rows": 1878134}, {"name": "hlt_ReestrMH", "rows": 182}, {"name": "hlt_ReestrMHSMTAP", "rows": 610236}, {"name": "hlt_ReestrTAPMH", "rows": 372890}, {"name": "hlt_RegMedicalCheck", "rows": 49962}, {"name": "hlt_SMTAP", "rows": 1534142}, {"name": "hlt_TAP", "rows": 1727450}, {"name": "oms_DocumentStatus", "rows": 4}, {"name": "oms_kl_DDService", "rows": 17}, {"name": "Oms_LPU", "rows": 2610}, {"name": "Oms_mkb", "rows": 16418}, {"name": "oms_ParamValue", "rows": 28229554}, {"name": "Oms_PRVD", "rows": 329}, {"name": "oms_ServiceMedical", "rows": 19846}, {"name": "oms_StateBirth", "rows": 5}, {"name": "smp_MedService", "rows": 1007}, {"name": "stt_Bed", "rows": 1584}, {"name": "stt_BedAction", "rows": 69516}, {"name": "stt_Dead", "rows": 1479}, {"name": "stt_Diagnos", "rows": 918098}, {"name": "stt_EmerSign", "rows": 4}, {"name": "stt_MedicalHistory", "rows": 241760}, {"name": "stt_MedServicePatient", "rows": 258671}, {"name": "stt_MigrationPatient", "rows": 869917}, {"name": "stt_NewbornHistory", "rows": 6616}, {"name": "stt_OperationPurpose", "rows": 76091}, {"name": "stt_ProcedureList", "rows": 58}, {"name": "stt_Reanimation", "rows": 12408}, {"name": "stt_ReestrMKSB", "rows": 90102}, {"name": "stt_ReestrOccasion", "rows": 39859}, {"name": "stt_StationarBranch", "rows": 139}, {"name": "stt_SurgicalOperation", "rows": 60556}]} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..0b38dbd --- /dev/null +++ b/main.py @@ -0,0 +1,7 @@ +from app.api import app +from app.cli import main + + +if __name__ == "__main__": + exit_code = main() + exit(exit_code) diff --git a/req.txt b/req.txt new file mode 100644 index 0000000..58ba55d --- /dev/null +++ b/req.txt @@ -0,0 +1,7 @@ +pandas +sqlalchemy +pymssql +psycopg2-binary +fastapi +uvicorn +python-dotenv diff --git a/sql/nightly_stt_medicalhistory_schedule.sql b/sql/nightly_stt_medicalhistory_schedule.sql new file mode 100644 index 0000000..748b95b --- /dev/null +++ b/sql/nightly_stt_medicalhistory_schedule.sql @@ -0,0 +1,73 @@ +WITH schedule_payload AS ( + SELECT + 'Nightly stt_MedicalHistory'::text AS name, + 'daily'::text AS schedule_type, + TRUE AS enabled, + FALSE AS catch_up_missed_runs, + '["stt_MedicalHistory"]'::jsonb AS tables_json, + TRUE AS send_email, + FALSE AS dry_run, + NULL::integer AS read_limit, + NULL::integer AS interval_seconds, + '02:00:00'::text AS daily_time, + NULL::timestamp AS start_at, + CASE + WHEN localtime < time '02:00:00' + THEN date_trunc('day', now()) + time '02:00:00' + ELSE date_trunc('day', now()) + interval '1 day' + time '02:00:00' + END AS next_run_at +), +updated AS ( + UPDATE replicator.migration_schedules AS schedules + SET + updated_at = now(), + schedule_type = payload.schedule_type, + enabled = payload.enabled, + catch_up_missed_runs = payload.catch_up_missed_runs, + tables_json = payload.tables_json, + send_email = payload.send_email, + dry_run = payload.dry_run, + read_limit = payload.read_limit, + interval_seconds = payload.interval_seconds, + daily_time = payload.daily_time, + start_at = payload.start_at, + next_run_at = payload.next_run_at + FROM schedule_payload AS payload + WHERE schedules.name = payload.name + RETURNING schedules.schedule_id +) +INSERT INTO replicator.migration_schedules ( + schedule_id, + created_at, + updated_at, + name, + schedule_type, + enabled, + catch_up_missed_runs, + tables_json, + send_email, + dry_run, + read_limit, + interval_seconds, + daily_time, + start_at, + next_run_at +) +SELECT + md5(random()::text || clock_timestamp()::text), + now(), + now(), + payload.name, + payload.schedule_type, + payload.enabled, + payload.catch_up_missed_runs, + payload.tables_json, + payload.send_email, + payload.dry_run, + payload.read_limit, + payload.interval_seconds, + payload.daily_time, + payload.start_at, + payload.next_run_at +FROM schedule_payload AS payload +WHERE NOT EXISTS (SELECT 1 FROM updated); diff --git a/sql/seed_migration_tables.sql b/sql/seed_migration_tables.sql new file mode 100644 index 0000000..96c741b --- /dev/null +++ b/sql/seed_migration_tables.sql @@ -0,0 +1,49 @@ +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_Card', 'hlt_disp_card', 'incremental', 'full_then_incremental', 'Life_hlt_disp_Card', 'x_DateTime', 'disp_CardLifeID', '["x_DateTime", "disp_CardLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_CardID"]', '["disp_CardLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_ProcedureList', 'stt_procedurelist', 'incremental', 'full_then_incremental', 'Life_stt_ProcedureList', 'x_DateTime', 'ProcedureListLifeID', '["x_DateTime", "ProcedureListLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ProcedureListID"]', '["ProcedureListLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_StationarBranch', 'stt_stationarbranch', 'incremental', 'full_then_incremental', 'Life_stt_StationarBranch', 'x_DateTime', 'StationarBranchLifeID', '["x_DateTime", "StationarBranchLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["StationarBranchID"]', '["StationarBranchLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_ReestrIdCase', 'hlt_reestridcase', 'incremental', 'full_then_incremental', 'Life_hlt_ReestrIdCase', 'x_DateTime', 'ReestrIdCaseLifeID', '["x_DateTime", "ReestrIdCaseLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrIdCaseID"]', '["ReestrIdCaseLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('Oms_PRVD', 'oms_prvd', 'incremental', 'full_then_incremental', 'Life_Oms_PRVD', 'x_DateTime', 'PRVDLifeID', '["x_DateTime", "PRVDLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["PRVDID"]', '["PRVDLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_BedAction', 'stt_bedaction', 'incremental', 'full_then_incremental', 'Life_stt_BedAction', 'x_DateTime', 'BedActionLifeID', '["x_DateTime", "BedActionLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["BedActionID"]', '["BedActionLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_Exam', 'hlt_disp_exam', 'incremental', 'full_then_incremental', 'Life_hlt_disp_Exam', 'x_DateTime', 'disp_ExamLifeID', '["x_DateTime", "disp_ExamLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_ExamID"]', '["disp_ExamLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('smp_MedService', 'smp_medservice', 'incremental', 'full_then_incremental', 'Life_smp_MedService', 'x_DateTime', 'MedServiceLifeID', '["x_DateTime", "MedServiceLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MedServiceID"]', '["MedServiceLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('oms_DocumentStatus', 'oms_documentstatus', 'incremental', 'full_then_incremental', 'Life_oms_DocumentStatus', 'x_DateTime', 'DocumentStatusLifeID', '["x_DateTime", "DocumentStatusLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["DocumentStatusID"]', '["DocumentStatusLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('die_Card', 'die_card', 'incremental', 'full_then_incremental', 'Life_die_Card', 'x_DateTime', 'CardLifeID', '["x_DateTime", "CardLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["CardID"]', '["CardLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('Oms_LPU', 'oms_lpu', 'incremental', 'full_then_incremental', 'Life_oms_LPU', 'x_DateTime', 'LPULifeID', '["x_DateTime", "LPULifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["LPUID"]', '["LPULifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 09:30:15.367815', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_MKB_TAP', 'hlt_mkb_tap', 'incremental', 'full_then_incremental', 'Life_hlt_MKB_TAP', 'x_DateTime', 'MKB_TAPLifeID', '["x_DateTime", "MKB_TAPLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MKB_TAPID"]', '["MKB_TAPLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_ReestrTAPMH', 'hlt_reestrtapmh', 'incremental', 'full_then_incremental', 'Life_hlt_ReestrTAPMH', 'x_DateTime', 'ReestrTAPMHLifeID', '["x_DateTime", "ReestrTAPMHLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrTAPMHID"]', '["ReestrTAPMHLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('oms_kl_DDService', 'oms_kl_ddservice', 'incremental', 'full_then_incremental', 'Life_oms_kl_DDService', 'x_DateTime', 'kl_DDServiceLifeID', '["x_DateTime", "kl_DDServiceLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["kl_DDServiceID"]', '["kl_DDServiceLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_MigrationPatient', 'stt_migrationpatient', 'incremental', 'full_then_incremental', 'Life_stt_MigrationPatient', 'x_DateTime', 'MigrationPatientLifeID', '["x_DateTime", "MigrationPatientLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MigrationPatientID"]', '["MigrationPatientLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('die_Mkb', 'die_mkb', 'incremental', 'full_then_incremental', 'Life_die_Mkb', 'x_DateTime', 'MkbLifeID', '["x_DateTime", "MkbLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MkbID"]', '["MkbLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_OperationPurpose', 'stt_operationpurpose', 'incremental', 'full_then_incremental', 'Life_stt_OperationPurpose', 'x_DateTime', 'OperationPurposeLifeID', '["x_DateTime", "OperationPurposeLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["OperationPurposeID"]', '["OperationPurposeLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_Dead', 'stt_dead', 'incremental', 'full_then_incremental', 'Life_stt_Dead', 'x_DateTime', 'DeadLifeID', '["x_DateTime", "DeadLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["DeadID"]', '["DeadLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_Invoice', 'hlt_invoice', 'incremental', 'full_then_incremental', 'Life_hlt_Invoice', 'x_DateTime', 'InvoiceLifeID', '["x_DateTime", "InvoiceLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["InvoiceID"]', '["InvoiceLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_ReestrOccasion', 'stt_reestroccasion', 'incremental', 'full_then_incremental', 'Life_stt_ReestrOccasion', 'x_DateTime', 'ReestrOccasionLifeID', '["x_DateTime", "ReestrOccasionLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrOccasionID"]', '["ReestrOccasionLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_ReestrMKSB', 'stt_reestrmksb', 'incremental', 'full_then_incremental', 'Life_stt_ReestrMKSB', 'x_DateTime', 'ReestrMKSBLifeID', '["x_DateTime", "ReestrMKSBLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrMKSBID"]', '["ReestrMKSBLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('oms_StateBirth', 'oms_statebirth', 'incremental', 'full_then_incremental', 'Life_oms_StateBirth', 'x_DateTime', 'StateBirthLifeID', '["x_DateTime", "StateBirthLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["StateBirthID"]', '["StateBirthLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_EmerSign', 'stt_emersign', 'incremental', 'full_then_incremental', 'Life_stt_EmerSign', 'x_DateTime', 'EmerSignLifeID', '["x_DateTime", "EmerSignLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["EmerSignID"]', '["EmerSignLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_ReestrMHSMTAP', 'hlt_reestrmhsmtap', 'incremental', 'full_then_incremental', 'Life_hlt_ReestrMHSMTAP', 'x_DateTime', 'ReestrMHSMTAPLifeID', '["x_DateTime", "ReestrMHSMTAPLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrMHSMTAPID"]', '["ReestrMHSMTAPLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('die_Certificate', 'die_certificate', 'incremental', 'full_then_incremental', 'Life_die_Certificate', 'x_DateTime', 'CertificateLifeID', '["x_DateTime", "CertificateLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["CertificateID"]', '["CertificateLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_PolisMKAB', 'hlt_polismkab', 'incremental', 'full_then_incremental', 'Life_hlt_PolisMKAB', 'x_DateTime', 'PolisMKABLifeID', '["x_DateTime", "PolisMKABLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["PolisMKABID"]', '["PolisMKABLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('oms_ParamValue', 'oms_paramvalue', 'incremental', 'full_then_incremental', 'Life_oms_ParamValue', 'x_DateTime', 'ParamValueLifeID', '["x_DateTime", "ParamValueLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ParamValueID"]', '["ParamValueLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_dent_tech_OrderSM', 'hlt_dent_tech_ordersm', 'incremental', 'full_then_incremental', 'Life_hlt_dent_tech_OrderSM', 'x_DateTime', 'dent_tech_OrderSMLifeID', '["x_DateTime", "dent_tech_OrderSMLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["dent_tech_OrderSMID"]', '["dent_tech_OrderSMLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_CardPlan', 'hlt_disp_cardplan', 'incremental', 'full_then_incremental', 'Life_hlt_disp_CardPlan', 'x_DateTime', 'disp_CardPlanLifeID', '["x_DateTime", "disp_CardPlanLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_CardPlanID"]', '["disp_CardPlanLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_DUVisit', 'hlt_duvisit', 'incremental', 'full_then_incremental', 'Life_hlt_DUVisit', 'x_DateTime', 'DUVisitLifeID', '["x_DateTime", "DUVisitLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["DUVisitID"]', '["DUVisitLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_PatientModel', 'hlt_disp_patientmodel', 'incremental', 'full_then_incremental', 'Life_hlt_disp_PatientModel', 'x_DateTime', 'disp_PatientModelLifeID', '["x_DateTime", "disp_PatientModelLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_PatientModelID"]', '["disp_PatientModelLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('oms_ServiceMedical', 'oms_servicemedical', 'incremental', 'full_then_incremental', 'Life_oms_ServiceMedical', 'x_DateTime', 'ServiceMedicalLifeID', '["x_DateTime", "ServiceMedicalLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ServiceMedicalID"]', '["ServiceMedicalLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_Diagnos', 'stt_diagnos', 'incremental', 'full_then_incremental', 'Life_stt_Diagnos', 'x_DateTime', 'DiagnosLifeID', '["x_DateTime", "DiagnosLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["DiagnosID"]', '["DiagnosLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_TAP', 'hlt_tap', 'incremental', 'full_then_incremental', 'Life_hlt_TAP', 'x_DateTime', 'TAPLifeID', '["x_DateTime", "TAPLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["TAPID"]', '["TAPLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_MKAB', 'hlt_mkab', 'incremental', 'full_then_incremental', 'Life_hlt_MKAB', 'x_DateTime', 'MKABLifeID', '["x_DateTime", "MKABLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MKABID"]', '["MKABLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_LPUDoctor', 'hlt_lpudoctor', 'incremental', 'full_then_incremental', 'Life_hlt_LPUDoctor', 'x_DateTime', 'LPUDoctorLifeID', '["x_DateTime", "LPUDoctorLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["LPUDoctorID"]', '["LPUDoctorLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_SurgicalOperation', 'stt_surgicaloperation', 'incremental', 'full_then_incremental', 'Life_stt_SurgicalOperation', 'x_DateTime', 'SurgicalOperationLifeID', '["x_DateTime", "SurgicalOperationLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["SurgicalOperationID"]', '["SurgicalOperationLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_DocPRVD', 'hlt_docprvd', 'incremental', 'full_then_incremental', 'Life_hlt_DocPRVD', 'x_DateTime', 'DocPRVDLifeID', '["x_DateTime", "DocPRVDLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["DocPRVDID"]', '["DocPRVDLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('Oms_mkb', 'oms_mkb', 'incremental', 'full_then_incremental', 'Life_Oms_mkb', 'x_DateTime', 'MKBLifeID', '["x_DateTime", "MKBLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MKBID"]', '["MKBLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_NewbornHistory', 'stt_newbornhistory', 'incremental', 'full_then_incremental', 'Life_stt_NewbornHistory', 'x_DateTime', 'NewbornHistoryLifeID', '["x_DateTime", "NewbornHistoryLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["NewbornHistoryID"]', '["NewbornHistoryID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_MedicalHistory', 'stt_medicalhistory', 'incremental', 'full_then_incremental', 'Life_stt_MedicalHistory', 'x_DateTime', 'MedicalHistoryLifeID', '["x_DateTime", "MedicalHistoryLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MedicalHistoryID"]', '["MedicalHistoryLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_RegMedicalCheck', 'hlt_regmedicalcheck', 'incremental', 'full_then_incremental', 'Life_hlt_RegMedicalCheck', 'x_DateTime', 'RegMedicalCheckLifeID', '["x_DateTime", "RegMedicalCheckLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["RegMedicalCheckID"]', '["RegMedicalCheckLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_Bed', 'stt_bed', 'incremental', 'full_then_incremental', 'Life_stt_Bed', 'x_DateTime', 'BedLifeID', '["x_DateTime", "BedLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["BedID"]', '["BedLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_SMTAP', 'hlt_smtap', 'incremental', 'full_then_incremental', 'Life_hlt_SMTAP', 'x_DateTime', 'SMTAPLifeID', '["x_DateTime", "SMTAPLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["SMTAPID"]', '["SMTAPLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_Type', 'hlt_disp_type', 'incremental', 'full_then_incremental', 'Life_hlt_disp_Type', 'x_DateTime', 'disp_TypeLifeID', '["x_DateTime", "disp_TypeLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_TypeID"]', '["disp_TypeLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('stt_MedServicePatient', 'stt_medservicepatient', 'incremental', 'full_then_incremental', 'Life_stt_MedServicePatient', 'x_DateTime', 'MedServicePatientLifeID', '["x_DateTime", "MedServicePatientLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["MedServicePatientID"]', '["MedServicePatientLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_BillService', 'hlt_billservice', 'incremental', 'full_then_incremental', 'Life_hlt_BillService', 'x_DateTime', 'BillServiceLifeID', '["x_DateTime", "BillServiceLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["BillServiceID"]', '["BillServiceLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_ReestrMH', 'hlt_reestrmh', 'incremental', 'full_then_incremental', 'Life_hlt_ReestrMH', 'x_DateTime', 'ReestrMHLifeID', '["x_DateTime", "ReestrMHLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["ReestrMHID"]', '["ReestrMHLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557'); +INSERT INTO replicator.migration_tables (source_table, target_table, mode, initial_load_mode, life_table, datetime_column, sequence_column, order_columns_json, operation_column, delete_operations_json, upsert_operations_json, primary_key_json, exclude_columns_json, timescale, timescale_time_column, enabled, created_at, updated_at) VALUES ('hlt_disp_ServicePM', 'hlt_disp_servicepm', 'incremental', 'full_then_incremental', 'Life_hlt_disp_ServicePM', 'x_DateTime', 'disp_ServicePMLifeID', '["x_DateTime", "disp_ServicePMLifeID"]', 'x_Operation', '["d"]', '["i", "u"]', '["disp_ServicePMID"]', '["disp_ServicePMLifeID", "x_Operation", "x_DateTime", "x_Seance", "x_User"]', false, null, true, '2026-04-13 07:02:42.339557', '2026-04-13 07:02:42.339557');