4.0.0: Исправлена обработка старых дампов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
This commit is contained in:
85
.env.test
Normal file
85
.env.test
Normal 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
|
||||||
120
play-life-backend/MIGRATION_BASELINE.md
Normal file
120
play-life-backend/MIGRATION_BASELINE.md
Normal 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`
|
||||||
|
|
||||||
|
Приложение автоматически проверяет оба пути.
|
||||||
458
play-life-backend/MIGRATION_RISKS_AND_SOLUTIONS.md
Normal file
458
play-life-backend/MIGRATION_RISKS_AND_SOLUTIONS.md
Normal 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 "Проверьте работу приложения"
|
||||||
|
```
|
||||||
168
play-life-backend/apply_baseline.sh
Executable file
168
play-life-backend/apply_baseline.sh
Executable 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 "Теперь приложение будет автоматически применять новые миграции при запуске."
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
module play-eng-backend
|
module play-eng-backend
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/chromedp/chromedp v0.14.2
|
github.com/chromedp/chromedp v0.14.2
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
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-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -22,5 +23,5 @@ require (
|
|||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
|
||||||
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
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 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
|
||||||
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
|
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 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
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 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
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 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
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 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
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=
|
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/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 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
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/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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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 h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
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 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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=
|
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
3
play-life-backend/migrations/000001_baseline.down.sql
Normal file
3
play-life-backend/migrations/000001_baseline.down.sql
Normal 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
|
||||||
497
play-life-backend/migrations/000001_baseline.up.sql
Normal file
497
play-life-backend/migrations/000001_baseline.up.sql
Normal 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);
|
||||||
15
play-life-backend/migrations_old/README.md
Normal file
15
play-life-backend/migrations_old/README.md
Normal 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>` для создания новых миграций.
|
||||||
347
play-life-backend/test_baseline.sh
Executable file
347
play-life-backend/test_baseline.sh
Executable 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 ""
|
||||||
144
play-life-backend/validate_baseline.sh
Executable file
144
play-life-backend/validate_baseline.sh
Executable 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"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.28.4",
|
"version": "4.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user