4.0.0: Исправлена обработка старых дампов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s

This commit is contained in:
poignatov
2026-01-25 16:41:50 +03:00
parent b8ef59bfd1
commit 90643c504a
42 changed files with 2052 additions and 1157 deletions

85
.env.test Normal file
View File

@@ -0,0 +1,85 @@
# ============================================
# Единый файл конфигурации для всех проектов
# Backend и Play-Life-Web
# ============================================
# ============================================
# Database Configuration
# ============================================
DB_HOST=localhost
DB_PORT=5432
DB_USER=playeng
DB_PASSWORD=playeng
DB_NAME=playeng_migration_test_1769347550
# ============================================
# Backend Server Configuration
# ============================================
# Порт для backend сервера (по умолчанию: 8080)
# В production всегда используется порт 8080 внутри контейнера
PORT=8080
# ============================================
# Play Life Web Configuration
# ============================================
# Порт для frontend приложения play-life-web
WEB_PORT=3001
# ============================================
# Telegram Bot Configuration
# ============================================
# Токен единого бота для всех пользователей
# Получить у @BotFather: https://t.me/botfather
TELEGRAM_BOT_TOKEN=your-bot-token-here
# Base URL для автоматической настройки webhook
# Примеры:
# - Для production с HTTPS: https://your-domain.com
# - Для локальной разработки с ngrok: https://abc123.ngrok.io
# - Для прямого доступа на нестандартном порту: http://your-server:8080
# Webhook будет настроен автоматически при старте сервера на: <WEBHOOK_BASE_URL>/webhook/telegram
# Если не указан, webhook нужно настраивать вручную
WEBHOOK_BASE_URL=https://your-domain.com
# ============================================
# Todoist Integration Configuration
# ============================================
# Единое Todoist приложение для всех пользователей Play Life
# Настроить в: https://developer.todoist.com/appconsole.html
#
# В настройках Todoist приложения указать:
# - OAuth Redirect URL: <WEBHOOK_BASE_URL>/api/integrations/todoist/oauth/callback
# - Webhooks callback URL: <WEBHOOK_BASE_URL>/webhook/todoist
# - Watched events: item:completed
# Client ID единого Todoist приложения
TODOIST_CLIENT_ID=
# Client Secret единого Todoist приложения
TODOIST_CLIENT_SECRET=
# Секрет для проверки подлинности webhook от Todoist (опционально)
# Получить в Developer Console: "Client secret for webhooks"
TODOIST_WEBHOOK_SECRET=
# ============================================
# Authentication Configuration
# ============================================
# Секретный ключ для подписи JWT токенов
# ВАЖНО: Обязательно задайте свой уникальный секретный ключ для production!
# Если не задан, будет использован случайно сгенерированный (не рекомендуется для production)
# Можно сгенерировать с помощью: openssl rand -base64 32
JWT_SECRET=your-super-secret-jwt-key-change-in-production
# ============================================
# Scheduler Configuration
# ============================================
# Часовой пояс для планировщика задач (например: Europe/Moscow, America/New_York, UTC)
# Используется для:
# - Автоматической фиксации целей на неделю каждый понедельник в 6:00
# - Отправки ежедневного отчёта в 23:59
# ВАЖНО: Укажите правильный часовой пояс, иначе задачи будут срабатывать в UTC!
# Список доступных часовых поясов: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TIMEZONE=Europe/Moscow
DB_NAME=playeng_migration_test_1769347550

View File

@@ -1 +1 @@
3.28.4
4.0.0

View File

@@ -0,0 +1,120 @@
# Инструкция по применению baseline миграции
## Обзор
После перехода на `golang-migrate` текущая схема БД была зафиксирована как baseline миграция `000001_baseline.up.sql`. Для существующих баз данных baseline миграция **не должна применяться автоматически** - вместо этого нужно использовать команду `migrate force` для установки текущей версии миграции.
## Для существующих баз данных
### Шаг 1: Создание backup
**ОБЯЗАТЕЛЬНО** создайте backup базы данных перед применением baseline:
```bash
# Используйте существующий скрипт dump-db.sh
./dump-db.sh
# Или вручную:
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > backup_$(date +%Y%m%d_%H%M%S).sql
```
### Шаг 2: Установка версии миграции
Для существующих баз данных нужно установить версию миграции в `1` (baseline), не применяя саму миграцию:
```bash
# Установите переменные окружения
export DB_HOST=localhost
export DB_PORT=5432
export DB_USER=playeng
export DB_PASSWORD=playeng
export DB_NAME=playeng
# Установите версию миграции в 1 (baseline)
migrate -path ./play-life-backend/migrations \
-database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable" \
force 1
```
**Важно:** Команда `force 1` устанавливает версию миграции в `1`, но **не выполняет** SQL из baseline миграции. Это правильно, так как схема уже существует.
### Шаг 3: Проверка
Проверьте, что версия миграции установлена правильно:
```bash
migrate -path ./play-life-backend/migrations \
-database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable" \
version
```
Должно вывести: `1 (dirty)`
Если выводит `1 (dirty)`, это нормально - это означает, что версия установлена, но миграция не была применена (что и требуется для baseline).
### Шаг 4: Очистка dirty флага (опционально)
Если нужно убрать dirty флаг:
```bash
migrate -path ./play-life-backend/migrations \
-database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable" \
force 1
```
## Для новых баз данных
Для новых баз данных baseline миграция применится автоматически при первом запуске приложения через функцию `runMigrations()`.
Или вручную:
```bash
migrate -path ./play-life-backend/migrations \
-database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable" \
up
```
## Проверка схемы
После применения baseline (или установки версии для существующих БД) можно проверить схему:
```bash
# Экспорт схемы
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME --schema-only > current_schema.sql
# Сравнение с baseline (если нужно)
diff current_schema.sql play-life-backend/migrations/000001_baseline.up.sql
```
## Важные замечания
1. **Никогда не применяйте baseline миграцию на существующих БД** - используйте только `migrate force 1`
2. **Всегда создавайте backup** перед любыми операциями с миграциями
3. **Проверяйте версию миграции** после установки baseline
4. **Новые миграции** будут применяться автоматически при запуске приложения
## Устранение проблем
### Ошибка "dirty database version"
Если база данных находится в состоянии "dirty", исправьте это:
```bash
migrate -path ./play-life-backend/migrations \
-database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable" \
force <version>
```
Где `<version>` - текущая версия миграции (обычно 1 для baseline).
### Ошибка "no change"
Если при применении миграций вы видите "no change", это нормально - база данных уже на актуальной версии.
### Проблемы с путями к миграциям
Убедитесь, что путь к миграциям правильный:
- Локально: `./play-life-backend/migrations`
- В Docker: `/migrations`
Приложение автоматически проверяет оба пути.

View File

@@ -0,0 +1,458 @@
# Анализ рисков миграции на golang-migrate с baseline
## Критические риски
### 1. Потеря данных при неправильном применении baseline
**Риск**: При применении baseline миграции на существующую БД может произойти:
- Попытка создать уже существующие таблицы (ошибка)
- Потеря данных при DROP/CREATE операциях
- Конфликты с существующими данными
**Вероятность**: Средняя
**Влияние**: Критическое
**Решения**:
1. **Обязательный backup перед применением**
```bash
# Создать backup перед миграцией
./dump-db.sh --env-file .env baseline-migration-backup
```
2. **Использование `migrate force` вместо `migrate up` для существующих БД**
```bash
# Для существующих БД - установить версию без применения
migrate -path ./migrations -database "postgres://..." force 1
```
3. **Проверка существования таблиц в baseline миграции**
- Использовать `CREATE TABLE IF NOT EXISTS` (но это не идеально для baseline)
- Или создать скрипт проверки перед применением
4. **Тестирование на dev окружении**
- Сначала применить на dev БД
- Проверить целостность данных
- Только потом применять на production
---
### 2. Ошибки в baseline миграции (неполная схема)
**Риск**: Baseline миграция может не включать:
- Некоторые таблицы или колонки
- Индексы или constraints
- Materialized views
- Начальные данные (словарь с id=0)
- Sequences с правильными значениями
**Вероятность**: Высокая
**Влияние**: Критическое
**Решения**:
1. **Автоматическая проверка полноты схемы**
```bash
# Создать скрипт для сравнения текущей схемы с baseline
# Использовать pg_dump --schema-only для сравнения
pg_dump --schema-only -h $DB_HOST -U $DB_USER -d $DB_NAME > current_schema.sql
# Сравнить с baseline миграцией
```
2. **Пошаговая сборка baseline**
- Собрать схему из всех init*DB функций
- Добавить все миграции 012-029
- Проверить через `pg_dump --schema-only` на актуальной БД
3. **Тестирование baseline на чистой БД**
```bash
# Создать тестовую БД
createdb test_baseline
# Применить baseline
migrate -path ./migrations -database "postgres://.../test_baseline" up
# Сравнить схему с production
```
4. **Валидация через SQL проверки**
- Добавить в baseline проверки существования всех таблиц
- Использовать `DO $$ BEGIN ... END $$;` блоки для валидации
---
### 3. Проблемы с sequences и начальными данными
**Риск**:
- Sequences могут быть не синхронизированы
- Начальные данные (словарь id=0) могут конфликтовать
- Автоинкременты могут начаться с неправильного значения
**Вероятность**: Средняя
**Влияние**: Среднее
**Решения**:
1. **Правильная настройка sequences в baseline**
```sql
-- После создания таблицы и вставки данных
SELECT setval('dictionaries_id_seq',
(SELECT MAX(id) FROM dictionaries),
true);
```
2. **Использование ON CONFLICT для начальных данных**
```sql
INSERT INTO dictionaries (id, name)
VALUES (0, 'Все слова')
ON CONFLICT (id) DO NOTHING;
```
3. **Проверка sequences после baseline**
```sql
-- Скрипт для проверки всех sequences
SELECT schemaname, sequencename, last_value
FROM pg_sequences;
```
---
### 4. Проблемы с materialized views
**Риск**:
- Materialized view может не создаться корректно
- Зависимости от таблиц могут быть нарушены
- Данные в MV могут быть неактуальными
**Вероятность**: Средняя
**Влияние**: Среднее
**Решения**:
1. **Создание MV после всех таблиц**
- Убедиться, что все таблицы созданы до создания MV
- Использовать `DROP MATERIALIZED VIEW IF EXISTS` перед созданием
2. **Обновление данных после создания**
```sql
-- После создания MV
REFRESH MATERIALIZED VIEW weekly_report_mv;
```
3. **Проверка зависимостей**
```sql
-- Проверить зависимости MV
SELECT * FROM pg_depend
WHERE objid = 'weekly_report_mv'::regclass;
```
---
### 5. Конфликты версий миграций
**Риск**:
- Таблица `schema_migrations` может быть в неправильном состоянии
- Версия может быть установлена неправильно
- Конфликт между старой и новой системой миграций
**Вероятность**: Средняя
**Влияние**: Высокое
**Решения**:
1. **Проверка состояния schema_migrations перед применением**
```go
// Проверить, существует ли таблица schema_migrations
// Если да - проверить текущую версию
var version uint
err := db.QueryRow("SELECT version FROM schema_migrations LIMIT 1").Scan(&version)
```
2. **Очистка старой таблицы (если была)**
```sql
-- Если была старая таблица миграций
DROP TABLE IF EXISTS old_migrations_table;
```
3. **Использование `migrate force` только для существующих БД**
- Новые БД должны использовать `migrate up`
- Существующие БД - `migrate force 1`
---
### 6. Проблемы с окружениями (dev/prod различия)
**Риск**:
- Различия в схемах между dev и prod
- Разные версии PostgreSQL
- Разные настройки БД
**Вероятность**: Средняя
**Влияние**: Высокое
**Решения**:
1. **Проверка версии PostgreSQL**
```sql
SELECT version();
```
2. **Тестирование на всех окружениях**
- Dev окружение
- Staging (если есть)
- Production (после успешного тестирования)
3. **Документирование различий**
- Зафиксировать версию PostgreSQL
- Зафиксировать настройки БД
---
### 7. Проблемы с откатом (rollback)
**Риск**:
- Baseline миграция не может быть откачена
- Ошибки при откате последующих миграций
- Потеря данных при откате
**Вероятность**: Низкая
**Влияние**: Высокое
**Решения**:
1. **Baseline не откатывается (по дизайну)**
- Пустой `000001_baseline.down.sql`
- Документировать это ограничение
2. **Правильные down миграции для новых миграций**
- Каждая новая миграция должна иметь корректный down файл
- Тестировать откат на dev окружении
3. **Backup перед откатом**
- Всегда создавать backup перед откатом
- Особенно на production
---
### 8. Проблемы при старте приложения
**Риск**:
- Миграции могут не примениться при старте
- Ошибки подключения к БД во время миграций
- Таймауты при применении миграций
**Вероятность**: Средняя
**Влияние**: Высокое
**Решения**:
1. **Обработка ошибок миграций**
```go
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"postgres", driver)
if err != nil {
log.Fatal("Failed to initialize migrations:", err)
}
if err := m.Up(); err != nil {
if err != migrate.ErrNoChange {
log.Fatal("Failed to apply migrations:", err)
}
log.Println("Database is up to date")
}
```
2. **Retry логика для подключения к БД**
- Уже есть в коде (10 попыток)
- Применить перед миграциями
3. **Таймауты для миграций**
```go
// Установить таймаут для миграций
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
```
4. **Логирование процесса миграций**
- Логировать каждую применяемую миграцию
- Логировать ошибки с деталями
---
### 9. Проблемы с Docker и путями к миграциям
**Риск**:
- Миграции могут не найтись в контейнере
- Неправильные пути к файлам миграций
- Проблемы с правами доступа
**Вероятность**: Низкая
**Влияние**: Среднее
**Решения**:
1. **Проверка путей в Dockerfile**
```dockerfile
# Убедиться, что миграции копируются
COPY play-life-backend/migrations /migrations
```
2. **Использование абсолютных путей**
```go
migrationsPath := "/migrations"
if _, err := os.Stat(migrationsPath); os.IsNotExist(err) {
// Fallback для локальной разработки
migrationsPath = "play-life-backend/migrations"
}
```
3. **Проверка доступности миграций при старте**
```go
// Проверить, что папка миграций существует
if _, err := os.Stat(migrationsPath); os.IsNotExist(err) {
log.Fatal("Migrations directory not found:", migrationsPath)
}
```
---
### 10. Проблемы с параллельным доступом
**Риск**:
- Несколько инстансов приложения могут пытаться применить миграции одновременно
- Конфликты при применении миграций
**Вероятность**: Низкая
**Влияние**: Высокое
**Решения**:
1. **Блокировки на уровне БД**
- golang-migrate использует транзакции
- PostgreSQL блокирует таблицу schema_migrations
2. **Применение миграций только в одном инстансе**
- Использовать флаг `--migrate` для запуска миграций
- Или применять миграции отдельным процессом
3. **Проверка версии перед применением**
```go
version, dirty, err := m.Version()
if dirty {
log.Fatal("Database is in dirty state, manual intervention required")
}
```
---
## План митигации рисков
### Этап 1: Подготовка (до применения baseline)
1. ✅ Создать backup всех БД (dev, staging, prod)
2. ✅ Собрать полную схему через `pg_dump --schema-only`
3. ✅ Создать baseline миграцию на основе схемы
4. ✅ Протестировать baseline на чистой БД
5. ✅ Сравнить схему после baseline с текущей схемой
### Этап 2: Тестирование (на dev окружении)
1. ✅ Применить baseline через `migrate force 1`
2. ✅ Проверить целостность данных
3. ✅ Проверить работу приложения
4. ✅ Проверить sequences и начальные данные
5. ✅ Проверить materialized views
### Этап 3: Применение (на production)
1. ✅ Создать backup production БД
2. ✅ Применить baseline через `migrate force 1`
3. ✅ Проверить работу приложения
4. ✅ Мониторинг в течение первых часов
### Этап 4: Мониторинг (после применения)
1. ✅ Проверить логи приложения
2. ✅ Проверить ошибки БД
3. ✅ Проверить производительность
4. ✅ Собрать обратную связь от пользователей
---
## Чеклист перед применением baseline
- [ ] Backup всех БД создан и проверен
- [ ] Baseline миграция протестирована на чистой БД
- [ ] Схема после baseline совпадает с текущей схемой
- [ ] Тестирование на dev окружении успешно
- [ ] Инструкции по применению baseline готовы
- [ ] Команда проинформирована о миграции
- [ ] Окно для миграции запланировано (для production)
- [ ] План отката подготовлен (если что-то пойдет не так)
---
## Скрипты для проверки
### Скрипт проверки схемы
```bash
#!/bin/bash
# check_schema.sh - Проверка полноты baseline миграции
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-playeng}
DB_PASSWORD=${DB_PASSWORD:-playeng}
DB_NAME=${DB_NAME:-playeng}
echo "Проверка схемы БД..."
# Получить список всех таблиц
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
" > current_tables.txt
echo "Таблицы в БД сохранены в current_tables.txt"
echo "Сравните с таблицами в baseline миграции"
```
### Скрипт применения baseline
```bash
#!/bin/bash
# apply_baseline.sh - Безопасное применение baseline
set -e
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-playeng}
DB_PASSWORD=${DB_PASSWORD:-playeng}
DB_NAME=${DB_NAME:-playeng}
DATABASE_URL="postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable"
echo "⚠️ ВНИМАНИЕ: Это применит baseline миграцию!"
echo "База данных: $DB_NAME"
echo "Хост: $DB_HOST:$DB_PORT"
read -p "Вы уверены? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "Отменено"
exit 1
fi
# Создать backup
echo "Создание backup..."
./dump-db.sh --env-file .env baseline-backup-$(date +%Y%m%d_%H%M%S)
# Применить baseline
echo "Применение baseline..."
migrate -path ./play-life-backend/migrations -database "$DATABASE_URL" force 1
echo "✅ Baseline применен успешно"
echo "Проверьте работу приложения"
```

View File

@@ -0,0 +1,168 @@
#!/bin/bash
# Безопасный скрипт для применения baseline миграции к существующим БД
# Включает создание backup, проверки и применение baseline
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Получаем переменные окружения
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-playeng}
DB_PASSWORD=${DB_PASSWORD:-playeng}
DB_NAME=${DB_NAME:-playeng}
MIGRATIONS_PATH="play-life-backend/migrations"
BACKUP_DIR="../database-dumps"
echo "=== Применение baseline миграции ==="
echo ""
# Проверяем наличие необходимых инструментов
if ! command -v migrate &> /dev/null; then
echo -e "${RED}Ошибка: migrate не найден. Установите golang-migrate:${NC}"
echo " brew install golang-migrate"
echo " или"
echo " go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest"
exit 1
fi
if ! command -v pg_dump &> /dev/null; then
echo -e "${RED}Ошибка: pg_dump не найден. Установите PostgreSQL client tools.${NC}"
exit 1
fi
# Проверяем наличие директории миграций
if [ ! -d "$MIGRATIONS_PATH" ]; then
echo -e "${RED}Ошибка: Директория миграций не найдена: $MIGRATIONS_PATH${NC}"
exit 1
fi
# Проверяем наличие baseline миграции
if [ ! -f "$MIGRATIONS_PATH/000001_baseline.up.sql" ]; then
echo -e "${RED}Ошибка: Baseline миграция не найдена: $MIGRATIONS_PATH/000001_baseline.up.sql${NC}"
exit 1
fi
echo "Параметры подключения:"
echo " Host: $DB_HOST"
echo " Port: $DB_PORT"
echo " User: $DB_USER"
echo " Database: $DB_NAME"
echo ""
# Проверяем подключение к БД
echo "1. Проверка подключения к БД..."
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
-c "SELECT 1;" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось подключиться к БД${NC}"
exit 1
fi
echo -e "${GREEN}✓ Подключение успешно${NC}"
echo ""
# Проверяем текущую версию миграции
echo "2. Проверка текущей версии миграции..."
DATABASE_URL="postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME?sslmode=disable"
CURRENT_VERSION=$(migrate -path "$MIGRATIONS_PATH" -database "$DATABASE_URL" version 2>&1 || echo "none")
if echo "$CURRENT_VERSION" | grep -q "dirty"; then
echo -e "${YELLOW}⚠ База данных находится в состоянии 'dirty'${NC}"
echo " Это нормально для baseline - будет исправлено"
elif echo "$CURRENT_VERSION" | grep -q "^[0-9]"; then
VERSION_NUM=$(echo "$CURRENT_VERSION" | grep -oE "^[0-9]+" || echo "0")
if [ "$VERSION_NUM" -ge 1 ]; then
echo -e "${GREEN}✓ Версия миграции уже установлена: $VERSION_NUM${NC}"
echo " Baseline уже применен, дальнейшие действия не требуются"
exit 0
fi
fi
echo " Текущая версия: $CURRENT_VERSION"
echo ""
# Создаем backup
echo "3. Создание backup БД..."
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/baseline_backup_$(date +%Y%m%d_%H%M%S).sql.gz"
PGPASSWORD=$DB_PASSWORD pg_dump \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
| gzip > "$BACKUP_FILE"
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось создать backup${NC}"
exit 1
fi
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo -e "${GREEN}✓ Backup создан: $BACKUP_FILE (размер: $BACKUP_SIZE)${NC}"
echo ""
# Подтверждение
echo "4. Подтверждение применения baseline..."
echo ""
echo -e "${YELLOW}ВНИМАНИЕ:${NC}"
echo " Будет установлена версия миграции в 1 (baseline)"
echo " Сама миграция НЕ будет применена (схема уже существует)"
echo " Backup сохранен в: $BACKUP_FILE"
echo ""
read -p "Продолжить? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Отменено пользователем"
exit 0
fi
# Применяем baseline (force 1)
echo ""
echo "5. Установка версии миграции в 1 (baseline)..."
migrate -path "$MIGRATIONS_PATH" \
-database "$DATABASE_URL" \
force 1
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось установить версию миграции${NC}"
echo " Backup доступен в: $BACKUP_FILE"
exit 1
fi
echo -e "${GREEN}✓ Версия миграции установлена${NC}"
echo ""
# Проверяем результат
echo "6. Проверка результата..."
FINAL_VERSION=$(migrate -path "$MIGRATIONS_PATH" -database "$DATABASE_URL" version 2>&1)
echo " Версия миграции: $FINAL_VERSION"
if echo "$FINAL_VERSION" | grep -qE "^1"; then
echo -e "${GREEN}✓ Baseline успешно применен!${NC}"
else
echo -e "${YELLOW}⚠ Версия миграции: $FINAL_VERSION${NC}"
echo " Это может быть нормально, если база в состоянии 'dirty'"
fi
echo ""
echo "=== Готово ==="
echo ""
echo "Backup сохранен в: $BACKUP_FILE"
echo "Версия миграции установлена в: 1 (baseline)"
echo ""
echo "Теперь приложение будет автоматически применять новые миграции при запуске."

View File

@@ -1,17 +1,18 @@
module play-eng-backend
go 1.24
go 1.24.0
require (
github.com/chromedp/chromedp v0.14.2
github.com/disintegration/imaging v1.6.2
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/gorilla/mux v1.8.1
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.45.0
)
require (
@@ -22,5 +23,5 @@ require (
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)

View File

@@ -1,13 +1,39 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -16,8 +42,12 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -26,15 +56,43 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
-- Baseline migration cannot be rolled back
-- This is the initial state of the database schema
-- If you need to revert, you must manually drop all tables and recreate from scratch

View File

@@ -0,0 +1,497 @@
-- Baseline Migration: Complete database schema
-- This migration represents the current state of the database schema
-- For existing databases, use: migrate force 1 (do not run this migration)
-- For new databases, this will create the complete schema
-- ============================================
-- Core Tables (no dependencies)
-- ============================================
-- Users table (base for multi-tenancy)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
last_login_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_users_email ON users(email);
-- Dictionaries table
CREATE TABLE dictionaries (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_dictionaries_user_id ON dictionaries(user_id);
-- Insert default dictionary with id = 0
DO $$
BEGIN
-- Set sequence to -1 so next value will be 0
PERFORM setval('dictionaries_id_seq', -1, false);
-- Insert the default dictionary with id = 0
INSERT INTO dictionaries (id, name)
VALUES (0, 'Все слова')
ON CONFLICT (id) DO NOTHING;
-- Set the sequence to start from 1 (so next auto-increment will be 1)
PERFORM setval('dictionaries_id_seq', 1, false);
EXCEPTION
WHEN others THEN
-- If sequence doesn't exist or other error, try without sequence manipulation
INSERT INTO dictionaries (id, name)
VALUES (0, 'Все слова')
ON CONFLICT (id) DO NOTHING;
END $$;
-- Projects table
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
priority SMALLINT,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_projects_deleted ON projects(deleted);
CREATE INDEX idx_projects_user_id ON projects(user_id);
-- Entries table
CREATE TABLE entries (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
created_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_entries_user_id ON entries(user_id);
-- ============================================
-- Dependent Tables
-- ============================================
-- Words table (depends on dictionaries, users)
CREATE TABLE words (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
translation TEXT NOT NULL,
description TEXT,
dictionary_id INTEGER NOT NULL DEFAULT 0 REFERENCES dictionaries(id),
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_words_dictionary_id ON words(dictionary_id);
CREATE INDEX idx_words_user_id ON words(user_id);
-- Progress table (depends on words, users)
CREATE TABLE progress (
id SERIAL PRIMARY KEY,
word_id INTEGER NOT NULL REFERENCES words(id) ON DELETE CASCADE,
success INTEGER DEFAULT 0,
failure INTEGER DEFAULT 0,
last_success_at TIMESTAMP,
last_failure_at TIMESTAMP,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT progress_word_user_unique UNIQUE (word_id, user_id)
);
CREATE INDEX idx_progress_user_id ON progress(user_id);
CREATE UNIQUE INDEX idx_progress_word_user_unique ON progress(word_id, user_id);
-- Configs table (depends on users)
CREATE TABLE configs (
id SERIAL PRIMARY KEY,
words_count INTEGER NOT NULL,
max_cards INTEGER,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_configs_user_id ON configs(user_id);
-- Config dictionaries table (depends on configs, dictionaries)
CREATE TABLE config_dictionaries (
config_id INTEGER NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
dictionary_id INTEGER NOT NULL REFERENCES dictionaries(id) ON DELETE CASCADE,
PRIMARY KEY (config_id, dictionary_id)
);
CREATE INDEX idx_config_dictionaries_config_id ON config_dictionaries(config_id);
CREATE INDEX idx_config_dictionaries_dictionary_id ON config_dictionaries(dictionary_id);
-- Nodes table (depends on projects, entries, users)
CREATE TABLE nodes (
id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
score NUMERIC(8,4),
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_nodes_project_id ON nodes(project_id);
CREATE INDEX idx_nodes_entry_id ON nodes(entry_id);
CREATE INDEX idx_nodes_user_id ON nodes(user_id);
-- Weekly goals table (depends on projects, users)
CREATE TABLE weekly_goals (
id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
goal_year INTEGER NOT NULL,
goal_week INTEGER NOT NULL,
min_goal_score NUMERIC(10,4) NOT NULL DEFAULT 0,
max_goal_score NUMERIC(10,4),
max_score NUMERIC(10,4),
priority SMALLINT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT weekly_goals_project_id_goal_year_goal_week_key UNIQUE (project_id, goal_year, goal_week)
);
CREATE INDEX idx_weekly_goals_project_id ON weekly_goals(project_id);
CREATE INDEX idx_weekly_goals_user_id ON weekly_goals(user_id);
-- Tasks table (depends on users)
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
completed INTEGER DEFAULT 0,
last_completed_at TIMESTAMP WITH TIME ZONE,
parent_task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
reward_message TEXT,
progression_base NUMERIC(10,4),
deleted BOOLEAN DEFAULT FALSE,
repetition_period INTERVAL,
next_show_at TIMESTAMP WITH TIME ZONE,
repetition_date TEXT,
config_id INTEGER REFERENCES configs(id) ON DELETE SET NULL,
wishlist_id INTEGER,
reward_policy VARCHAR(20) DEFAULT 'personal'
);
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_parent_task_id ON tasks(parent_task_id);
CREATE INDEX idx_tasks_deleted ON tasks(deleted);
CREATE INDEX idx_tasks_last_completed_at ON tasks(last_completed_at);
CREATE INDEX idx_tasks_config_id ON tasks(config_id);
CREATE UNIQUE INDEX idx_tasks_config_id_unique ON tasks(config_id) WHERE config_id IS NOT NULL AND deleted = FALSE;
CREATE INDEX idx_tasks_wishlist_id ON tasks(wishlist_id);
CREATE UNIQUE INDEX idx_tasks_wishlist_id_unique ON tasks(wishlist_id) WHERE wishlist_id IS NOT NULL AND deleted = FALSE;
CREATE INDEX idx_tasks_id_user_deleted ON tasks(id, user_id, deleted) WHERE deleted = FALSE;
CREATE INDEX idx_tasks_parent_deleted_covering ON tasks(parent_task_id, deleted, id)
INCLUDE (name, completed, last_completed_at, reward_message, progression_base)
WHERE deleted = FALSE;
COMMENT ON COLUMN tasks.config_id IS 'Link to test config. NULL if task is not a test.';
COMMENT ON COLUMN tasks.reward_policy IS 'For wishlist tasks: personal = only if user completes, shared = anyone completes';
-- Reward configs table (depends on tasks, projects)
CREATE TABLE reward_configs (
id SERIAL PRIMARY KEY,
position INTEGER NOT NULL,
task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
value NUMERIC(10,4) NOT NULL,
use_progression BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_reward_configs_task_id ON reward_configs(task_id);
CREATE INDEX idx_reward_configs_project_id ON reward_configs(project_id);
CREATE UNIQUE INDEX idx_reward_configs_task_position ON reward_configs(task_id, position);
-- Telegram integrations table (depends on users)
CREATE TABLE telegram_integrations (
id SERIAL PRIMARY KEY,
chat_id VARCHAR(255),
telegram_user_id BIGINT,
start_token VARCHAR(255),
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX idx_telegram_integrations_user_id_unique ON telegram_integrations(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_telegram_integrations_user_id ON telegram_integrations(user_id);
CREATE UNIQUE INDEX idx_telegram_integrations_start_token ON telegram_integrations(start_token) WHERE start_token IS NOT NULL;
CREATE UNIQUE INDEX idx_telegram_integrations_telegram_user_id ON telegram_integrations(telegram_user_id) WHERE telegram_user_id IS NOT NULL;
CREATE INDEX idx_telegram_integrations_chat_id ON telegram_integrations(chat_id) WHERE chat_id IS NOT NULL;
-- Todoist integrations table (depends on users)
CREATE TABLE todoist_integrations (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
todoist_user_id BIGINT,
todoist_email VARCHAR(255),
access_token TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT todoist_integrations_user_id_unique UNIQUE (user_id)
);
CREATE INDEX idx_todoist_integrations_user_id ON todoist_integrations(user_id);
CREATE UNIQUE INDEX idx_todoist_integrations_todoist_user_id ON todoist_integrations(todoist_user_id) WHERE todoist_user_id IS NOT NULL;
CREATE UNIQUE INDEX idx_todoist_integrations_todoist_email ON todoist_integrations(todoist_email) WHERE todoist_email IS NOT NULL;
-- Wishlist boards table (depends on users)
CREATE TABLE wishlist_boards (
id SERIAL PRIMARY KEY,
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
invite_token VARCHAR(64) UNIQUE,
invite_enabled BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_wishlist_boards_owner_id ON wishlist_boards(owner_id);
CREATE INDEX idx_wishlist_boards_invite_token ON wishlist_boards(invite_token) WHERE invite_token IS NOT NULL;
CREATE INDEX idx_wishlist_boards_owner_deleted ON wishlist_boards(owner_id, deleted);
-- Wishlist board members table (depends on wishlist_boards, users)
CREATE TABLE wishlist_board_members (
id SERIAL PRIMARY KEY,
board_id INTEGER NOT NULL REFERENCES wishlist_boards(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_board_member UNIQUE (board_id, user_id)
);
CREATE INDEX idx_board_members_board_id ON wishlist_board_members(board_id);
CREATE INDEX idx_board_members_user_id ON wishlist_board_members(user_id);
-- Wishlist items table (depends on users, wishlist_boards)
CREATE TABLE wishlist_items (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
price NUMERIC(10,2),
image_path VARCHAR(500),
link TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN DEFAULT FALSE,
board_id INTEGER REFERENCES wishlist_boards(id) ON DELETE CASCADE,
author_id INTEGER REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_wishlist_items_user_id ON wishlist_items(user_id);
CREATE INDEX idx_wishlist_items_user_deleted ON wishlist_items(user_id, deleted);
CREATE INDEX idx_wishlist_items_user_completed ON wishlist_items(user_id, completed, deleted);
CREATE INDEX idx_wishlist_items_board_id ON wishlist_items(board_id);
CREATE INDEX idx_wishlist_items_author_id ON wishlist_items(author_id);
CREATE INDEX idx_wishlist_items_id_deleted_covering ON wishlist_items(id, deleted)
INCLUDE (name)
WHERE deleted = FALSE;
-- Add foreign key for tasks.wishlist_id after wishlist_items is created
ALTER TABLE tasks ADD CONSTRAINT tasks_wishlist_id_fkey
FOREIGN KEY (wishlist_id) REFERENCES wishlist_items(id) ON DELETE SET NULL;
COMMENT ON TABLE wishlist_items IS 'Wishlist items for users';
COMMENT ON COLUMN wishlist_items.completed IS 'Flag indicating item was purchased/received';
COMMENT ON COLUMN wishlist_items.image_path IS 'Path to image file relative to uploads root';
-- Task conditions table (depends on tasks)
CREATE TABLE task_conditions (
id SERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_task_condition UNIQUE (task_id)
);
CREATE INDEX idx_task_conditions_task_id ON task_conditions(task_id);
COMMENT ON TABLE task_conditions IS 'Reusable unlock conditions based on task completion';
-- Score conditions table (depends on projects, users)
CREATE TABLE score_conditions (
id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
required_points NUMERIC(10,4) NOT NULL,
start_date DATE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT unique_score_condition UNIQUE (project_id, required_points, start_date)
);
CREATE INDEX idx_score_conditions_project_id ON score_conditions(project_id);
CREATE INDEX idx_score_conditions_user_id ON score_conditions(user_id);
COMMENT ON TABLE score_conditions IS 'Reusable unlock conditions based on project points';
COMMENT ON COLUMN score_conditions.start_date IS 'Date from which to start counting points. NULL means count all time.';
-- Wishlist conditions table (depends on wishlist_items, task_conditions, score_conditions, users)
CREATE TABLE wishlist_conditions (
id SERIAL PRIMARY KEY,
wishlist_item_id INTEGER NOT NULL REFERENCES wishlist_items(id) ON DELETE CASCADE,
task_condition_id INTEGER REFERENCES task_conditions(id) ON DELETE CASCADE,
score_condition_id INTEGER REFERENCES score_conditions(id) ON DELETE CASCADE,
display_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT check_exactly_one_condition CHECK (
(task_condition_id IS NOT NULL AND score_condition_id IS NULL) OR
(task_condition_id IS NULL AND score_condition_id IS NOT NULL)
)
);
CREATE INDEX idx_wishlist_conditions_item_id ON wishlist_conditions(wishlist_item_id);
CREATE INDEX idx_wishlist_conditions_item_order ON wishlist_conditions(wishlist_item_id, display_order);
CREATE INDEX idx_wishlist_conditions_task_condition_id ON wishlist_conditions(task_condition_id);
CREATE INDEX idx_wishlist_conditions_score_condition_id ON wishlist_conditions(score_condition_id);
CREATE INDEX idx_wishlist_conditions_user_id ON wishlist_conditions(user_id);
COMMENT ON TABLE wishlist_conditions IS 'Links between wishlist items and unlock conditions. Multiple conditions per item use AND logic.';
COMMENT ON COLUMN wishlist_conditions.display_order IS 'Order for displaying conditions in UI';
COMMENT ON COLUMN wishlist_conditions.user_id IS 'Owner of this condition. Each user has their own goals on shared boards';
-- Refresh tokens table (depends on users)
CREATE TABLE refresh_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
-- ============================================
-- Materialized Views
-- ============================================
-- Weekly report materialized view
CREATE MATERIALIZED VIEW weekly_report_mv AS
SELECT
p.id AS project_id,
agg.report_year,
agg.report_week,
COALESCE(agg.total_score, 0.0000) AS total_score,
CASE
WHEN wg.max_score IS NULL THEN COALESCE(agg.total_score, 0.0000)
ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_score)
END AS normalized_total_score
FROM
projects p
LEFT JOIN
(
SELECT
n.project_id,
EXTRACT(ISOYEAR FROM e.created_date)::INTEGER AS report_year,
EXTRACT(WEEK FROM e.created_date)::INTEGER AS report_week,
SUM(n.score) AS total_score
FROM
nodes n
JOIN
entries e ON n.entry_id = e.id
GROUP BY
1, 2, 3
) agg
ON p.id = agg.project_id
LEFT JOIN
weekly_goals wg
ON wg.project_id = p.id
AND wg.goal_year = agg.report_year
AND wg.goal_week = agg.report_week
WHERE
p.deleted = FALSE
ORDER BY
p.id, agg.report_year, agg.report_week
WITH DATA;
CREATE INDEX idx_weekly_report_mv_project_year_week ON weekly_report_mv(project_id, report_year, report_week);
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries. Includes all projects via LEFT JOIN. Adds normalized_total_score using weekly_goals.max_score snapshot.';
-- ============================================
-- Comments
-- ============================================
COMMENT ON TABLE configs IS 'Test configurations (words_count, max_cards, dictionary associations). Linked to tasks via tasks.config_id.';
COMMENT ON TABLE wishlist_boards IS 'Wishlist boards for organizing and sharing wishes';
COMMENT ON COLUMN wishlist_boards.invite_token IS 'Token for invite link, NULL = disabled';
COMMENT ON COLUMN wishlist_boards.invite_enabled IS 'Whether invite link is active';
COMMENT ON TABLE wishlist_board_members IS 'Users who joined boards via invite link (not owners)';
COMMENT ON COLUMN wishlist_items.author_id IS 'User who created this item (may differ from board owner on shared boards)';
COMMENT ON COLUMN wishlist_items.board_id IS 'Board this item belongs to';
-- ============================================
-- Additional Tables
-- ============================================
-- Eateries table
CREATE TABLE eateries (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255),
type VARCHAR(50),
distance DOUBLE PRECISION
);
-- Interesting places table
CREATE TABLE interesting_places (
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT,
description TEXT,
added_at TIMESTAMP WITH TIME ZONE,
is_visited BOOLEAN,
phone_number TEXT,
address TEXT,
updated_at TIMESTAMP WITH TIME ZONE
);
-- Music groups table
CREATE TABLE music_groups (
id INTEGER PRIMARY KEY,
name TEXT,
possible_locations TEXT
);
-- N8N chat histories table
CREATE TABLE n8n_chat_histories (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
message JSONB NOT NULL
);
-- Places to visit table
CREATE TABLE places_to_visit (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
description TEXT,
added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_visited BOOLEAN DEFAULT FALSE,
phone_number TEXT,
address TEXT,
updated_at TIMESTAMP WITH TIME ZONE
);
-- Restaurants table
CREATE TABLE restaurants (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255),
contact_info VARCHAR(255)
);
-- Upcoming concerts table (depends on music_groups)
CREATE TABLE upcoming_concerts (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES music_groups(id),
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
venue TEXT,
city TEXT,
tickets_url TEXT
);
CREATE UNIQUE INDEX idx_unique_concert ON upcoming_concerts(scheduled_at, city, group_id);

View File

@@ -0,0 +1,15 @@
# Архив старых миграций
Эта директория содержит старые SQL миграции (001-029), которые были заменены baseline миграцией `000001_baseline.up.sql`.
## Примечание
Эти миграции сохранены только для справки и истории. Они **не должны применяться** в новых установках или после перехода на golang-migrate.
## Новые миграции
Все новые миграции должны создаваться в формате golang-migrate:
- `000002_*.up.sql` - миграция вверх
- `000002_*.down.sql` - миграция вниз (откат)
Используйте команду `migrate create -ext sql -dir migrations -seq <name>` для создания новых миграций.

View File

@@ -0,0 +1,347 @@
#!/bin/bash
# Скрипт для тестирования baseline миграции на чистой БД
# Создает тестовую БД, применяет baseline, и сравнивает схему с production
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Получаем переменные окружения
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-playeng}
DB_PASSWORD=${DB_PASSWORD:-playeng}
DB_NAME=${DB_NAME:-playeng}
TEST_DB_NAME="playeng_baseline_test_$$"
MIGRATIONS_PATH="migrations"
TMP_DIR=$(mktemp -d)
echo "=== Тестирование baseline миграции на чистой БД ==="
echo ""
# Добавляем ~/go/bin в PATH если migrate не найден
if ! command -v migrate &> /dev/null; then
export PATH="$HOME/go/bin:$PATH"
fi
# Проверяем наличие необходимых инструментов
if ! command -v migrate &> /dev/null; then
echo -e "${RED}Ошибка: migrate не найден. Установите golang-migrate:${NC}"
echo " brew install golang-migrate"
echo " или"
echo " go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest"
exit 1
fi
# Определяем способ выполнения PostgreSQL команд
PG_DUMP_CMD=""
PG_PSQL_CMD=""
POSTGRES_CONTAINER=""
if command -v pg_dump &> /dev/null; then
PG_DUMP_CMD="pg_dump"
PG_PSQL_CMD="psql"
else
# Пытаемся найти PostgreSQL контейнер
if command -v docker &> /dev/null; then
POSTGRES_CONTAINER=$(docker ps --format "{{.Names}}" 2>/dev/null | grep -iE "(postgres|db)" | head -1)
if [ -n "$POSTGRES_CONTAINER" ]; then
PG_DUMP_CMD="docker exec $POSTGRES_CONTAINER pg_dump"
PG_PSQL_CMD="docker exec -i $POSTGRES_CONTAINER psql"
echo -e "${BLUE}Используется PostgreSQL из Docker контейнера: $POSTGRES_CONTAINER${NC}"
fi
fi
fi
HAS_PG_DUMP=false
if [ -n "$PG_DUMP_CMD" ]; then
HAS_PG_DUMP=true
else
echo -e "${YELLOW}Предупреждение: pg_dump не найден. Сравнение схем будет пропущено.${NC}"
echo " Для полного тестирования установите PostgreSQL client tools"
fi
# Проверяем наличие директории миграций
if [ ! -d "$MIGRATIONS_PATH" ]; then
echo -e "${RED}Ошибка: Директория миграций не найдена: $MIGRATIONS_PATH${NC}"
exit 1
fi
# Проверяем наличие baseline миграции
if [ ! -f "$MIGRATIONS_PATH/000001_baseline.up.sql" ]; then
echo -e "${RED}Ошибка: Baseline миграция не найдена: $MIGRATIONS_PATH/000001_baseline.up.sql${NC}"
exit 1
fi
echo "Параметры подключения:"
echo " Host: $DB_HOST"
echo " Port: $DB_PORT"
echo " User: $DB_USER"
echo " Test DB: $TEST_DB_NAME"
echo ""
# Проверяем подключение к БД
echo "1. Проверка подключения к БД..."
if [ -n "$POSTGRES_CONTAINER" ]; then
# Используем Docker
echo "SELECT 1;" | $PG_PSQL_CMD -U $DB_USER -d postgres > /dev/null 2>&1
elif [ -n "$PG_PSQL_CMD" ]; then
# Используем локальный psql
PGPASSWORD=$DB_PASSWORD $PG_PSQL_CMD \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "SELECT 1;" > /dev/null 2>&1
else
# Пытаемся через стандартный psql
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "SELECT 1;" > /dev/null 2>&1
fi
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось подключиться к БД${NC}"
exit 1
fi
echo -e "${GREEN}✓ Подключение успешно${NC}"
echo ""
# Создаем тестовую БД
echo "2. Создание тестовой БД..."
if [ -n "$POSTGRES_CONTAINER" ]; then
echo "CREATE DATABASE $TEST_DB_NAME;" | $PG_PSQL_CMD -U $DB_USER -d postgres > /dev/null 2>&1
elif [ -n "$PG_PSQL_CMD" ]; then
PGPASSWORD=$DB_PASSWORD $PG_PSQL_CMD \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "CREATE DATABASE $TEST_DB_NAME;" > /dev/null 2>&1
else
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "CREATE DATABASE $TEST_DB_NAME;" > /dev/null 2>&1
fi
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось создать тестовую БД${NC}"
exit 1
fi
echo -e "${GREEN}✓ Тестовая БД создана: $TEST_DB_NAME${NC}"
echo ""
# Ждем немного, чтобы БД точно создалась
sleep 1
# Проверяем, что БД создана
echo "3. Проверка существования тестовой БД..."
if [ -n "$POSTGRES_CONTAINER" ]; then
if echo "SELECT 1 FROM pg_database WHERE datname='$TEST_DB_NAME';" | $PG_PSQL_CMD -U $DB_USER -d postgres -t | grep -q 1; then
echo -e "${GREEN}✓ БД подтверждена${NC}"
else
echo -e "${RED}Ошибка: БД не найдена после создания${NC}"
exit 1
fi
fi
echo ""
# Применяем baseline миграцию
echo "4. Применение baseline миграции..."
cd "$(dirname "$0")" || exit 1
if [ -n "$POSTGRES_CONTAINER" ]; then
# Для Docker контейнеров используем psql напрямую, так как migrate может иметь проблемы с подключением
echo -e "${BLUE}Применение миграции через psql (Docker)...${NC}"
if [ -f "$MIGRATIONS_PATH/000001_baseline.up.sql" ]; then
if cat "$MIGRATIONS_PATH/000001_baseline.up.sql" | $PG_PSQL_CMD -U $DB_USER -d $TEST_DB_NAME > /dev/null 2>&1; then
echo -e "${GREEN}✓ Миграция применена через psql${NC}"
# Создаем таблицу schema_migrations вручную для migrate
echo "CREATE TABLE IF NOT EXISTS schema_migrations (version bigint NOT NULL PRIMARY KEY, dirty boolean NOT NULL);" | $PG_PSQL_CMD -U $DB_USER -d $TEST_DB_NAME > /dev/null 2>&1
echo "INSERT INTO schema_migrations (version, dirty) VALUES (1, false) ON CONFLICT (version) DO UPDATE SET dirty = false;" | $PG_PSQL_CMD -U $DB_USER -d $TEST_DB_NAME > /dev/null 2>&1
MIGRATE_SUCCESS=false # Устанавливаем в false, чтобы использовать psql для проверки версии
else
echo -e "${RED}Ошибка: Не удалось применить миграцию через psql${NC}"
exit 1
fi
else
echo -e "${RED}Ошибка: Файл миграции не найден${NC}"
exit 1
fi
DATABASE_URL="postgres://$DB_USER:$DB_PASSWORD@localhost:$DB_PORT/$TEST_DB_NAME?sslmode=disable"
else
DATABASE_URL="postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$TEST_DB_NAME?sslmode=disable"
if ! migrate -path "$MIGRATIONS_PATH" -database "$DATABASE_URL" up; then
echo -e "${RED}Ошибка: Не удалось применить baseline миграцию${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ Baseline миграция применена${NC}"
echo ""
# Проверяем версию миграции
echo "5. Проверка версии миграции..."
if [ -n "$POSTGRES_CONTAINER" ] && [ "${MIGRATE_SUCCESS:-false}" = "false" ]; then
# Проверяем версию через psql
VERSION=$(echo "SELECT version FROM schema_migrations;" | $PG_PSQL_CMD -U $DB_USER -d $TEST_DB_NAME -t 2>/dev/null | tr -d ' ' | head -1)
if [ -n "$VERSION" ] && [ "$VERSION" != "" ]; then
echo " Версия: $VERSION"
if [ "$VERSION" = "1" ]; then
echo -e "${GREEN}✓ Версия миграции корректна${NC}"
else
echo -e "${YELLOW}⚠ Неожиданная версия миграции: $VERSION${NC}"
fi
else
echo -e "${YELLOW}Не удалось определить версию миграции${NC}"
fi
else
# Используем migrate для проверки версии
VERSION=$(migrate -path "$MIGRATIONS_PATH" -database "$DATABASE_URL" version 2>&1)
echo " Версия: $VERSION"
if echo "$VERSION" | grep -qE "^1"; then
echo -e "${GREEN}✓ Версия миграции корректна${NC}"
else
echo -e "${YELLOW}⚠ Неожиданная версия миграции${NC}"
fi
fi
echo ""
# Экспортируем схему из тестовой БД (если pg_dump доступен)
if [ "$HAS_PG_DUMP" = true ]; then
echo "6. Экспорт схемы из тестовой БД..."
if [ -n "$POSTGRES_CONTAINER" ]; then
$PG_DUMP_CMD -U $DB_USER -d $TEST_DB_NAME --schema-only --no-owner --no-privileges > "$TMP_DIR/baseline_schema.sql"
else
PGPASSWORD=$DB_PASSWORD $PG_DUMP_CMD \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $TEST_DB_NAME \
--schema-only \
--no-owner \
--no-privileges \
-f "$TMP_DIR/baseline_schema.sql"
fi
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось экспортировать схему${NC}"
exit 1
fi
echo -e "${GREEN}✓ Схема экспортирована${NC}"
echo ""
# Пытаемся экспортировать схему из production БД для сравнения
echo "7. Экспорт схемы из production БД для сравнения..."
if [ -n "$POSTGRES_CONTAINER" ]; then
if $PG_DUMP_CMD -U $DB_USER -d $DB_NAME --schema-only --no-owner --no-privileges > "$TMP_DIR/production_schema.sql" 2>/dev/null; then
PROD_EXPORT_SUCCESS=true
else
PROD_EXPORT_SUCCESS=false
fi
else
if PGPASSWORD=$DB_PASSWORD $PG_DUMP_CMD \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
--schema-only \
--no-owner \
--no-privileges \
-f "$TMP_DIR/production_schema.sql" 2>/dev/null; then
PROD_EXPORT_SUCCESS=true
else
PROD_EXPORT_SUCCESS=false
fi
fi
if [ "$PROD_EXPORT_SUCCESS" = true ]; then
echo -e "${GREEN}✓ Схема production экспортирована${NC}"
echo ""
# Сравниваем схемы
echo "8. Сравнение схем..."
# Подсчитываем объекты
echo -e "${BLUE}Таблицы:${NC}"
BASELINE_TABLES=$(grep -c "CREATE TABLE" "$TMP_DIR/baseline_schema.sql" || echo "0")
PROD_TABLES=$(grep -c "CREATE TABLE" "$TMP_DIR/production_schema.sql" || echo "0")
echo " Baseline: $BASELINE_TABLES"
echo " Production: $PROD_TABLES"
if [ "$BASELINE_TABLES" -eq "$PROD_TABLES" ]; then
echo -e " ${GREEN}✓ Количество таблиц совпадает${NC}"
else
echo -e " ${YELLOW}⚠ Количество таблиц не совпадает${NC}"
fi
echo ""
echo -e "${BLUE}Индексы:${NC}"
BASELINE_INDEXES=$(grep -c "CREATE.*INDEX" "$TMP_DIR/baseline_schema.sql" || echo "0")
PROD_INDEXES=$(grep -c "CREATE.*INDEX" "$TMP_DIR/production_schema.sql" || echo "0")
echo " Baseline: $BASELINE_INDEXES"
echo " Production: $PROD_INDEXES"
if [ "$BASELINE_INDEXES" -eq "$PROD_INDEXES" ]; then
echo -e " ${GREEN}✓ Количество индексов совпадает${NC}"
else
echo -e " ${YELLOW}⚠ Количество индексов не совпадает${NC}"
fi
echo ""
echo -e "${BLUE}Materialized Views:${NC}"
BASELINE_MV=$(grep -c "CREATE MATERIALIZED VIEW" "$TMP_DIR/baseline_schema.sql" || echo "0")
PROD_MV=$(grep -c "CREATE MATERIALIZED VIEW" "$TMP_DIR/production_schema.sql" || echo "0")
echo " Baseline: $BASELINE_MV"
echo " Production: $PROD_MV"
if [ "$BASELINE_MV" -eq "$PROD_MV" ]; then
echo -e " ${GREEN}✓ Количество materialized views совпадает${NC}"
else
echo -e " ${YELLOW}⚠ Количество materialized views не совпадает${NC}"
fi
echo ""
echo "Для детального сравнения выполните:"
echo " diff $TMP_DIR/baseline_schema.sql $TMP_DIR/production_schema.sql"
echo ""
echo "Или используйте:"
echo " diff -u $TMP_DIR/baseline_schema.sql $TMP_DIR/production_schema.sql | less"
else
echo -e "${YELLOW}Не удалось экспортировать схему production БД${NC}"
echo " Продолжаем без сравнения"
echo ""
echo "Схема baseline сохранена в: $TMP_DIR/baseline_schema.sql"
fi
else
echo "6. Пропуск экспорта схемы (pg_dump недоступен)"
echo ""
echo -e "${YELLOW}Для полного тестирования установите PostgreSQL client tools:${NC}"
echo " macOS: brew install postgresql"
echo " или используйте Docker контейнер с PostgreSQL"
echo ""
fi
echo ""
echo "=== Тестирование завершено ==="
echo ""
echo -e "${GREEN}✓ Baseline миграция успешно применена к чистой БД${NC}"
echo ""

View File

@@ -0,0 +1,144 @@
#!/bin/bash
# Скрипт для проверки полноты baseline миграции
# Сравнивает текущую схему БД с baseline миграцией
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Получаем переменные окружения
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-5432}
DB_USER=${DB_USER:-playeng}
DB_PASSWORD=${DB_PASSWORD:-playeng}
DB_NAME=${DB_NAME:-playeng}
echo "=== Проверка полноты baseline миграции ==="
echo ""
# Проверяем наличие pg_dump
if ! command -v pg_dump &> /dev/null; then
echo -e "${RED}Ошибка: pg_dump не найден. Установите PostgreSQL client tools.${NC}"
exit 1
fi
# Создаем временную директорию
TMP_DIR=$(mktemp -d)
trap "rm -rf $TMP_DIR" EXIT
echo "1. Экспортируем текущую схему БД..."
PGPASSWORD=$DB_PASSWORD pg_dump \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d $DB_NAME \
--schema-only \
--no-owner \
--no-privileges \
-f "$TMP_DIR/current_schema.sql"
if [ $? -ne 0 ]; then
echo -e "${RED}Ошибка: Не удалось экспортировать схему БД${NC}"
exit 1
fi
echo -e "${GREEN}✓ Схема экспортирована${NC}"
echo ""
# Применяем baseline миграцию к временной БД для сравнения
echo "2. Создаем временную БД для проверки baseline..."
TEMP_DB_NAME="playeng_baseline_test_$$"
PGPASSWORD=$DB_PASSWORD psql \
-h $DB_HOST \
-p $DB_PORT \
-U $DB_USER \
-d postgres \
-c "CREATE DATABASE $TEMP_DB_NAME;" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${YELLOW}Предупреждение: Не удалось создать временную БД. Продолжаем без неё.${NC}"
TEMP_DB_NAME=""
else
echo -e "${GREEN}✓ Временная БД создана${NC}"
fi
# Очистка временной БД при выходе
if [ -n "$TEMP_DB_NAME" ]; then
trap "PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d postgres -c 'DROP DATABASE IF EXISTS $TEMP_DB_NAME;' > /dev/null 2>&1; rm -rf $TMP_DIR" EXIT
fi
echo ""
echo "3. Анализ схемы..."
# Извлекаем только CREATE TABLE, CREATE INDEX, CREATE VIEW и т.д. из текущей схемы
grep -E "^(CREATE|ALTER|COMMENT)" "$TMP_DIR/current_schema.sql" | \
sed 's/--.*$//' | \
tr -d '\n' | \
sed 's/;/;\n/g' | \
sort > "$TMP_DIR/current_clean.sql"
# Извлекаем из baseline миграции
BASELINE_FILE="play-life-backend/migrations/000001_baseline.up.sql"
if [ ! -f "$BASELINE_FILE" ]; then
echo -e "${RED}Ошибка: Baseline файл не найден: $BASELINE_FILE${NC}"
exit 1
fi
grep -E "^(CREATE|ALTER|COMMENT)" "$BASELINE_FILE" | \
sed 's/--.*$//' | \
tr -d '\n' | \
sed 's/;/;\n/g' | \
sort > "$TMP_DIR/baseline_clean.sql"
echo ""
echo "4. Сравнение..."
# Сравниваем количество таблиц
CURRENT_TABLES=$(grep -c "CREATE TABLE" "$TMP_DIR/current_schema.sql" || echo "0")
BASELINE_TABLES=$(grep -c "CREATE TABLE" "$BASELINE_FILE" || echo "0")
echo " Текущая БД: $CURRENT_TABLES таблиц"
echo " Baseline: $BASELINE_TABLES таблиц"
if [ "$CURRENT_TABLES" -ne "$BASELINE_TABLES" ]; then
echo -e "${YELLOW}⚠ Количество таблиц не совпадает${NC}"
else
echo -e "${GREEN}✓ Количество таблиц совпадает${NC}"
fi
# Сравниваем количество индексов
CURRENT_INDEXES=$(grep -c "CREATE.*INDEX" "$TMP_DIR/current_schema.sql" || echo "0")
BASELINE_INDEXES=$(grep -c "CREATE.*INDEX" "$BASELINE_FILE" || echo "0")
echo " Текущая БД: $CURRENT_INDEXES индексов"
echo " Baseline: $BASELINE_INDEXES индексов"
if [ "$CURRENT_INDEXES" -ne "$BASELINE_INDEXES" ]; then
echo -e "${YELLOW}⚠ Количество индексов не совпадает${NC}"
else
echo -e "${GREEN}✓ Количество индексов совпадает${NC}"
fi
# Проверяем наличие materialized view
CURRENT_MV=$(grep -c "CREATE MATERIALIZED VIEW" "$TMP_DIR/current_schema.sql" || echo "0")
BASELINE_MV=$(grep -c "CREATE MATERIALIZED VIEW" "$BASELINE_FILE" || echo "0")
echo " Текущая БД: $CURRENT_MV materialized views"
echo " Baseline: $BASELINE_MV materialized views"
if [ "$CURRENT_MV" -ne "$BASELINE_MV" ]; then
echo -e "${YELLOW}⚠ Количество materialized views не совпадает${NC}"
else
echo -e "${GREEN}✓ Количество materialized views совпадает${NC}"
fi
echo ""
echo "=== Проверка завершена ==="
echo ""
echo "Для детального сравнения выполните:"
echo " diff $TMP_DIR/current_schema.sql $BASELINE_FILE"

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "3.28.4",
"version": "4.0.0",
"type": "module",
"scripts": {
"dev": "vite",