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

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

View File

@@ -0,0 +1,106 @@
-- Migration: Create database schema for play-life project
-- This script creates all tables and materialized views needed for the project
-- ============================================
-- Table: projects
-- ============================================
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
priority SMALLINT,
CONSTRAINT unique_project_name UNIQUE (name)
);
-- ============================================
-- Table: entries
-- ============================================
-- This table stores entries with creation dates
-- Used in weekly_report_mv for grouping by week
CREATE TABLE IF NOT EXISTS entries (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
created_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- Table: nodes
-- ============================================
-- This table stores nodes linked to projects and entries
-- Contains score information used in weekly reports
CREATE TABLE IF NOT EXISTS 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)
);
-- Create index on project_id for better join performance
CREATE INDEX IF NOT EXISTS idx_nodes_project_id ON nodes(project_id);
-- Create index on entry_id for better join performance
CREATE INDEX IF NOT EXISTS idx_nodes_entry_id ON nodes(entry_id);
-- ============================================
-- Table: weekly_goals
-- ============================================
-- This table stores weekly goals for projects
CREATE TABLE IF NOT EXISTS 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),
actual_score NUMERIC(10,4) DEFAULT 0,
priority SMALLINT,
CONSTRAINT weekly_goals_project_id_goal_year_goal_week_key UNIQUE (project_id, goal_year, goal_week)
);
-- Create index on project_id for better join performance
CREATE INDEX IF NOT EXISTS idx_weekly_goals_project_id ON weekly_goals(project_id);
-- ============================================
-- Materialized View: weekly_report_mv
-- ============================================
CREATE MATERIALIZED VIEW IF NOT EXISTS weekly_report_mv AS
SELECT
p.id AS project_id,
agg.report_year,
agg.report_week,
-- Используем COALESCE для установки total_score в 0.0000, если нет данных (NULL)
COALESCE(agg.total_score, 0.0000) AS total_score
FROM
projects p
LEFT JOIN
(
-- 1. Предварительная агрегация: суммируем score по неделям
-- Используем ISOYEAR для корректной работы на границе года
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
-- 2. Присоединяем агрегированные данные ко ВСЕМ проектам
ON p.id = agg.project_id
ORDER BY
p.id, agg.report_year, agg.report_week;
-- Create index on materialized view for better query performance
CREATE INDEX IF NOT EXISTS idx_weekly_report_mv_project_year_week
ON weekly_report_mv(project_id, report_year, report_week);
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE projects IS 'Projects table storing project information with priority';
COMMENT ON TABLE entries IS 'Entries table storing entry creation timestamps';
COMMENT ON TABLE nodes IS 'Nodes table linking projects, entries and storing scores';
COMMENT ON TABLE weekly_goals IS 'Weekly goals for projects';
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries';

View File

@@ -0,0 +1,53 @@
-- Migration: Add dictionaries table and dictionary_id to words
-- This script creates the dictionaries table and adds dictionary_id field to words table
-- ============================================
-- Table: dictionaries
-- ============================================
CREATE TABLE IF NOT EXISTS dictionaries (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
-- Insert default dictionary "Все слова" with id = 0
-- Note: PostgreSQL SERIAL starts from 1, so we need to use a workaround
-- First, set the sequence to allow inserting 0, then insert, then reset sequence
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);
END $$;
-- ============================================
-- Alter words table: Add dictionary_id column
-- ============================================
ALTER TABLE words
ADD COLUMN IF NOT EXISTS dictionary_id INTEGER DEFAULT 0 REFERENCES dictionaries(id);
-- Update all existing words to have dictionary_id = 0
UPDATE words
SET dictionary_id = 0
WHERE dictionary_id IS NULL;
-- Make dictionary_id NOT NULL after setting default values
ALTER TABLE words
ALTER COLUMN dictionary_id SET NOT NULL,
ALTER COLUMN dictionary_id SET DEFAULT 0;
-- Create index on dictionary_id for better join performance
CREATE INDEX IF NOT EXISTS idx_words_dictionary_id ON words(dictionary_id);
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE dictionaries IS 'Dictionaries table storing dictionary information';
COMMENT ON COLUMN words.dictionary_id IS 'Reference to dictionary. Default is 0 (Все слова)';

View File

@@ -0,0 +1,11 @@
-- Migration: Remove UNIQUE constraint from words.name
-- This script removes the unique constraint on the name column in the words table
-- Drop the unique constraint on words.name if it exists
ALTER TABLE words
DROP CONSTRAINT IF EXISTS words_name_key;
-- Also try to drop constraint if it was created with different name
ALTER TABLE words
DROP CONSTRAINT IF EXISTS words_name_unique;

View File

@@ -0,0 +1,21 @@
-- Migration: Add config_dictionaries table (many-to-many relationship)
-- This script creates the config_dictionaries table linking configs and dictionaries
-- ============================================
-- Table: config_dictionaries
-- ============================================
CREATE TABLE IF NOT EXISTS 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 indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_config_dictionaries_config_id ON config_dictionaries(config_id);
CREATE INDEX IF NOT EXISTS idx_config_dictionaries_dictionary_id ON config_dictionaries(dictionary_id);
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE config_dictionaries IS 'Many-to-many relationship table linking configs and dictionaries. If no dictionaries are selected for a config, all dictionaries will be used.';

View File

@@ -0,0 +1,29 @@
-- Migration: Fix weekly_report_mv to use ISOYEAR instead of YEAR
-- This fixes incorrect week calculations at year boundaries
-- Date: 2024
-- Drop existing materialized view
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
-- Recreate materialized view with ISOYEAR
CREATE MATERIALIZED VIEW weekly_report_mv AS
SELECT
n.project_id,
-- 🔑 ГЛАВНОЕ ИСПРАВЛЕНИЕ: Используем ISOYEAR
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
WITH DATA;
-- Recreate index
CREATE INDEX IF NOT EXISTS 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';

View File

@@ -0,0 +1,48 @@
-- Migration: Fix weekly_report_mv structure to include all projects via LEFT JOIN
-- This ensures the view structure matches the code in main.go
-- Date: 2024-12-29
--
-- Issue: Migration 005 created the view without LEFT JOIN to projects table,
-- which means projects without data were not included in the view.
-- This migration fixes the structure to match main.go implementation.
-- Drop existing materialized view
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
-- Recreate materialized view with correct structure (LEFT JOIN with projects)
-- This ensures all projects are included, even if they have no data for a given week
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
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
WHERE
p.deleted = FALSE
ORDER BY
p.id, agg.report_year, agg.report_week
WITH DATA;
-- Recreate index
CREATE INDEX IF NOT EXISTS 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.';

View File

@@ -0,0 +1,13 @@
-- Migration: Add deleted field to projects table
-- This script adds a deleted boolean field to mark projects as deleted (soft delete)
-- Add deleted column to projects table
ALTER TABLE projects
ADD COLUMN IF NOT EXISTS deleted BOOLEAN NOT NULL DEFAULT FALSE;
-- Create index on deleted column for better query performance
CREATE INDEX IF NOT EXISTS idx_projects_deleted ON projects(deleted);
-- Add comment for documentation
COMMENT ON COLUMN projects.deleted IS 'Soft delete flag: TRUE if project is deleted, FALSE otherwise';

View File

@@ -0,0 +1,16 @@
-- Migration: Add telegram_integrations table
-- This script creates a table to store Telegram bot tokens and chat IDs
-- Create telegram_integrations table
CREATE TABLE IF NOT EXISTS telegram_integrations (
id SERIAL PRIMARY KEY,
chat_id VARCHAR(255),
bot_token VARCHAR(255)
);
-- Add comment for documentation
COMMENT ON TABLE telegram_integrations IS 'Stores Telegram bot tokens and chat IDs for integrations';
COMMENT ON COLUMN telegram_integrations.id IS 'Auto-increment primary key';
COMMENT ON COLUMN telegram_integrations.chat_id IS 'Telegram chat ID (nullable, set automatically after first message)';
COMMENT ON COLUMN telegram_integrations.bot_token IS 'Telegram bot token (nullable, set by user)';

View File

@@ -0,0 +1,128 @@
-- Migration: Add users table and user_id to all tables for multi-tenancy
-- This script adds user authentication and makes all data user-specific
-- All statements use IF NOT EXISTS / IF EXISTS for idempotency
-- ============================================
-- Table: users
-- ============================================
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_users_email ON users(email);
-- ============================================
-- Table: refresh_tokens
-- ============================================
CREATE TABLE IF NOT EXISTS 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 NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
-- ============================================
-- Add user_id to projects
-- ============================================
ALTER TABLE projects
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_projects_user_id ON projects(user_id);
-- Drop old unique constraint (name now unique per user, handled in app)
ALTER TABLE projects DROP CONSTRAINT IF EXISTS unique_project_name;
-- ============================================
-- Add user_id to entries
-- ============================================
ALTER TABLE entries
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_entries_user_id ON entries(user_id);
-- ============================================
-- Add user_id to dictionaries
-- ============================================
ALTER TABLE dictionaries
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_dictionaries_user_id ON dictionaries(user_id);
-- ============================================
-- Add user_id to words
-- ============================================
ALTER TABLE words
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_words_user_id ON words(user_id);
-- ============================================
-- Add user_id to progress
-- ============================================
ALTER TABLE progress
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_progress_user_id ON progress(user_id);
-- Drop old unique constraint (word_id now unique per user)
ALTER TABLE progress DROP CONSTRAINT IF EXISTS progress_word_id_key;
-- Create new unique constraint per user
CREATE UNIQUE INDEX IF NOT EXISTS idx_progress_word_user_unique ON progress(word_id, user_id);
-- ============================================
-- Add user_id to configs
-- ============================================
ALTER TABLE configs
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_configs_user_id ON configs(user_id);
-- ============================================
-- Add user_id to telegram_integrations
-- ============================================
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_telegram_integrations_user_id ON telegram_integrations(user_id);
-- ============================================
-- Add user_id to weekly_goals
-- ============================================
ALTER TABLE weekly_goals
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_weekly_goals_user_id ON weekly_goals(user_id);
-- ============================================
-- Add user_id to nodes (score data)
-- ============================================
ALTER TABLE nodes
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_nodes_user_id ON nodes(user_id);
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE users IS 'Users table for authentication and multi-tenancy';
COMMENT ON COLUMN users.email IS 'User email address (unique, used for login)';
COMMENT ON COLUMN users.password_hash IS 'Bcrypt hashed password';
COMMENT ON COLUMN users.name IS 'User display name';
COMMENT ON COLUMN users.is_active IS 'Whether the user account is active';
COMMENT ON TABLE refresh_tokens IS 'JWT refresh tokens for persistent login';
-- Note: The first user who logs in will automatically become the owner of all
-- existing data (projects, entries, dictionaries, words, etc.) that have NULL user_id.
-- This is handled in the application code (claimOrphanedData function).

View File

@@ -0,0 +1,17 @@
-- Migration: Add webhook_token to telegram_integrations
-- This allows identifying user by webhook URL token
-- Add webhook_token column to telegram_integrations
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS webhook_token VARCHAR(255);
-- Create unique index on webhook_token for fast lookups
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_webhook_token
ON telegram_integrations(webhook_token)
WHERE webhook_token IS NOT NULL;
-- Generate webhook tokens for existing integrations
-- This will be handled by application code, but we ensure the column exists
COMMENT ON COLUMN telegram_integrations.webhook_token IS 'Unique token for webhook URL identification (e.g., /webhook/telegram/{token})';

View File

@@ -0,0 +1,103 @@
-- Migration: Refactor telegram_integrations for single shared bot
-- and move Todoist webhook_token to separate table
-- ============================================
-- 1. Создаем таблицу todoist_integrations
-- ============================================
CREATE TABLE IF NOT EXISTS todoist_integrations (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
webhook_token VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT todoist_integrations_user_id_unique UNIQUE (user_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_webhook_token
ON todoist_integrations(webhook_token);
CREATE INDEX IF NOT EXISTS idx_todoist_integrations_user_id
ON todoist_integrations(user_id);
COMMENT ON TABLE todoist_integrations IS 'Todoist webhook integration settings per user';
COMMENT ON COLUMN todoist_integrations.webhook_token IS 'Unique token for Todoist webhook URL';
-- ============================================
-- 2. Мигрируем webhook_token из telegram_integrations в todoist_integrations
-- ============================================
INSERT INTO todoist_integrations (user_id, webhook_token, created_at, updated_at)
SELECT user_id, webhook_token, COALESCE(created_at, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP
FROM telegram_integrations
WHERE webhook_token IS NOT NULL
AND webhook_token != ''
AND user_id IS NOT NULL
ON CONFLICT (user_id) DO NOTHING;
-- ============================================
-- 3. Модифицируем telegram_integrations
-- ============================================
-- Удаляем bot_token (будет в .env)
ALTER TABLE telegram_integrations
DROP COLUMN IF EXISTS bot_token;
-- Удаляем webhook_token (перенесли в todoist_integrations)
ALTER TABLE telegram_integrations
DROP COLUMN IF EXISTS webhook_token;
-- Добавляем telegram_user_id
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS telegram_user_id BIGINT;
-- Добавляем start_token для deep links
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS start_token VARCHAR(255);
-- Добавляем timestamps если их нет
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE telegram_integrations
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
-- ============================================
-- 4. Создаем индексы
-- ============================================
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_start_token
ON telegram_integrations(start_token)
WHERE start_token IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_telegram_user_id
ON telegram_integrations(telegram_user_id)
WHERE telegram_user_id IS NOT NULL;
-- Уникальность user_id
DROP INDEX IF EXISTS idx_telegram_integrations_user_id;
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_integrations_user_id_unique
ON telegram_integrations(user_id)
WHERE user_id IS NOT NULL;
-- Индекс для поиска по chat_id
CREATE INDEX IF NOT EXISTS idx_telegram_integrations_chat_id
ON telegram_integrations(chat_id)
WHERE chat_id IS NOT NULL;
-- Удаляем старый индекс webhook_token
DROP INDEX IF EXISTS idx_telegram_integrations_webhook_token;
-- ============================================
-- 5. Очищаем данные Telegram для переподключения
-- ============================================
UPDATE telegram_integrations
SET chat_id = NULL,
telegram_user_id = NULL,
start_token = NULL,
updated_at = CURRENT_TIMESTAMP;
-- ============================================
-- Комментарии
-- ============================================
COMMENT ON COLUMN telegram_integrations.telegram_user_id IS 'Telegram user ID (message.from.id)';
COMMENT ON COLUMN telegram_integrations.chat_id IS 'Telegram chat ID для отправки сообщений';
COMMENT ON COLUMN telegram_integrations.start_token IS 'Временный токен для deep link при первом подключении';

View File

@@ -0,0 +1,45 @@
-- Migration: Refactor todoist_integrations for single Todoist app
-- Webhook теперь единый для всего приложения, токены в URL больше не нужны
-- Все пользователи используют одно Todoist приложение
-- ============================================
-- 1. Добавляем новые поля
-- ============================================
ALTER TABLE todoist_integrations
ADD COLUMN IF NOT EXISTS todoist_user_id BIGINT;
ALTER TABLE todoist_integrations
ADD COLUMN IF NOT EXISTS todoist_email VARCHAR(255);
ALTER TABLE todoist_integrations
ADD COLUMN IF NOT EXISTS access_token TEXT;
-- ============================================
-- 2. Удаляем webhook_token (больше не нужен!)
-- ============================================
ALTER TABLE todoist_integrations
DROP COLUMN IF EXISTS webhook_token;
-- ============================================
-- 3. Удаляем старый индекс на webhook_token
-- ============================================
DROP INDEX IF EXISTS idx_todoist_integrations_webhook_token;
-- ============================================
-- 4. Создаем новые индексы
-- ============================================
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_user_id
ON todoist_integrations(todoist_user_id)
WHERE todoist_user_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_todoist_integrations_todoist_email
ON todoist_integrations(todoist_email)
WHERE todoist_email IS NOT NULL;
-- ============================================
-- 5. Комментарии
-- ============================================
COMMENT ON COLUMN todoist_integrations.todoist_user_id IS 'Todoist user ID (from OAuth) - used to identify user in webhooks';
COMMENT ON COLUMN todoist_integrations.todoist_email IS 'Todoist user email (from OAuth)';
COMMENT ON COLUMN todoist_integrations.access_token IS 'Todoist OAuth access token (permanent)';

View File

@@ -0,0 +1,21 @@
-- Migration: Make refresh tokens permanent (no expiration)
-- Refresh tokens теперь не имеют срока действия (expires_at может быть NULL)
-- Access tokens живут 24 часа вместо 15 минут
-- ============================================
-- 1. Изменяем expires_at на NULLABLE
-- ============================================
ALTER TABLE refresh_tokens
ALTER COLUMN expires_at DROP NOT NULL;
-- ============================================
-- 2. Устанавливаем NULL для всех существующих токенов
-- (или можно оставить их как есть, если они еще не истекли)
-- ============================================
-- UPDATE refresh_tokens SET expires_at = NULL WHERE expires_at > NOW();
-- ============================================
-- 3. Комментарий
-- ============================================
COMMENT ON COLUMN refresh_tokens.expires_at IS 'Expiration date for refresh token. NULL means token never expires.';

View File

@@ -0,0 +1,58 @@
-- Migration: Add tasks and reward_configs tables
-- This script creates tables for task management system
-- ============================================
-- Table: tasks
-- ============================================
CREATE TABLE IF NOT EXISTS 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
);
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id ON tasks(parent_task_id);
CREATE INDEX IF NOT EXISTS idx_tasks_deleted ON tasks(deleted);
CREATE INDEX IF NOT EXISTS idx_tasks_last_completed_at ON tasks(last_completed_at);
-- ============================================
-- Table: reward_configs
-- ============================================
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_reward_configs_task_id ON reward_configs(task_id);
CREATE INDEX IF NOT EXISTS idx_reward_configs_project_id ON reward_configs(project_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_reward_configs_task_position ON reward_configs(task_id, position);
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE tasks IS 'Tasks table for task management system';
COMMENT ON COLUMN tasks.name IS 'Task name (required for main tasks, optional for subtasks)';
COMMENT ON COLUMN tasks.completed IS 'Number of times task was completed';
COMMENT ON COLUMN tasks.last_completed_at IS 'Date and time of last task completion';
COMMENT ON COLUMN tasks.parent_task_id IS 'Parent task ID for subtasks (NULL for main tasks)';
COMMENT ON COLUMN tasks.reward_message IS 'Reward message template with placeholders ${0}, ${1}, etc.';
COMMENT ON COLUMN tasks.progression_base IS 'Base value for progression calculation (NULL means no progression)';
COMMENT ON COLUMN tasks.deleted IS 'Soft delete flag';
COMMENT ON TABLE reward_configs IS 'Reward configurations for tasks';
COMMENT ON COLUMN reward_configs.position IS 'Position in reward_message template (0, 1, 2, etc.)';
COMMENT ON COLUMN reward_configs.task_id IS 'Task this reward belongs to';
COMMENT ON COLUMN reward_configs.project_id IS 'Project to add reward to';
COMMENT ON COLUMN reward_configs.value IS 'Default score value (can be negative)';
COMMENT ON COLUMN reward_configs.use_progression IS 'Whether to use progression multiplier for this reward';

View File

@@ -0,0 +1,14 @@
-- Migration: Add repetition_period field to tasks table
-- This script adds the repetition_period field for recurring tasks
-- ============================================
-- Add repetition_period column
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS repetition_period INTERVAL;
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON COLUMN tasks.repetition_period IS 'Period after which task should be repeated (NULL means task is not recurring)';

View File

@@ -0,0 +1,14 @@
-- Migration: Add next_show_at field to tasks table
-- This script adds the next_show_at field for postponing tasks
-- ============================================
-- Add next_show_at column
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS next_show_at TIMESTAMP WITH TIME ZONE;
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON COLUMN tasks.next_show_at IS 'Date when task should be shown again (NULL means use last_completed_at + period)';

View File

@@ -0,0 +1,16 @@
-- Migration: Add repetition_date field to tasks table
-- This script adds the repetition_date field for pattern-based recurring tasks
-- Format examples: "2 week" (2nd day of week), "15 month" (15th day of month), "02-01 year" (Feb 1st)
-- ============================================
-- Add repetition_date column
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS repetition_date TEXT;
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON COLUMN tasks.repetition_date IS 'Pattern-based repetition: "N week" (day of week 1-7), "N month" (day of month 1-31), "MM-DD year" (specific date). Mutually exclusive with repetition_period.';

View File

@@ -0,0 +1,86 @@
-- Migration: Add wishlist tables
-- This script creates tables for wishlist management system
-- Supports multiple unlock conditions per wishlist item (AND logic)
-- ============================================
-- Table: wishlist_items
-- ============================================
CREATE TABLE IF NOT EXISTS 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
);
CREATE INDEX IF NOT EXISTS idx_wishlist_items_user_id ON wishlist_items(user_id);
CREATE INDEX IF NOT EXISTS idx_wishlist_items_user_deleted ON wishlist_items(user_id, deleted);
CREATE INDEX IF NOT EXISTS idx_wishlist_items_user_completed ON wishlist_items(user_id, completed, deleted);
-- ============================================
-- Table: task_conditions
-- ============================================
-- Reusable conditions for task completion
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_task_conditions_task_id ON task_conditions(task_id);
-- ============================================
-- Table: score_conditions
-- ============================================
-- Reusable conditions for project points
CREATE TABLE IF NOT EXISTS score_conditions (
id SERIAL PRIMARY KEY,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
required_points NUMERIC(10,4) NOT NULL,
period_type VARCHAR(20), -- 'week', 'month', 'year', NULL (all time)
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_score_condition UNIQUE (project_id, required_points, period_type)
);
CREATE INDEX IF NOT EXISTS idx_score_conditions_project_id ON score_conditions(project_id);
-- ============================================
-- Table: wishlist_conditions
-- ============================================
-- Links wishlist items to unlock conditions
CREATE TABLE IF NOT EXISTS 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,
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 IF NOT EXISTS idx_wishlist_conditions_item_id ON wishlist_conditions(wishlist_item_id);
CREATE INDEX IF NOT EXISTS idx_wishlist_conditions_item_order ON wishlist_conditions(wishlist_item_id, display_order);
CREATE INDEX IF NOT EXISTS idx_wishlist_conditions_task_condition_id ON wishlist_conditions(task_condition_id);
CREATE INDEX IF NOT EXISTS idx_wishlist_conditions_score_condition_id ON wishlist_conditions(score_condition_id);
-- ============================================
-- Comments for documentation
-- ============================================
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';
COMMENT ON TABLE task_conditions IS 'Reusable unlock conditions based on task completion';
COMMENT ON TABLE score_conditions IS 'Reusable unlock conditions based on project points';
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';

View File

@@ -0,0 +1,37 @@
-- Migration: Change period_type to start_date in score_conditions
-- This allows specifying a start date for counting points instead of period type
-- Date can be in the past or future, NULL means count all time
-- Добавляем новое поле start_date
ALTER TABLE score_conditions
ADD COLUMN IF NOT EXISTS start_date DATE;
-- Миграция данных: для существующих записей с period_type устанавливаем start_date
-- Если period_type = 'week', то start_date = начало текущей недели
-- Если period_type = 'month', то start_date = начало текущего месяца
-- Если period_type = 'year', то start_date = начало текущего года
-- Если period_type IS NULL, то start_date = NULL (за всё время)
UPDATE score_conditions
SET start_date = CASE
WHEN period_type = 'week' THEN DATE_TRUNC('week', CURRENT_DATE)::DATE
WHEN period_type = 'month' THEN DATE_TRUNC('month', CURRENT_DATE)::DATE
WHEN period_type = 'year' THEN DATE_TRUNC('year', CURRENT_DATE)::DATE
ELSE NULL
END
WHERE start_date IS NULL;
-- Обновляем уникальное ограничение (удаляем старое, добавляем новое)
ALTER TABLE score_conditions
DROP CONSTRAINT IF EXISTS unique_score_condition;
ALTER TABLE score_conditions
ADD CONSTRAINT unique_score_condition
UNIQUE (project_id, required_points, start_date);
-- Обновляем комментарии
COMMENT ON COLUMN score_conditions.start_date IS 'Date from which to start counting points. NULL means count all time.';
-- Примечание: поле period_type оставляем пока для обратной совместимости
-- Его можно будет удалить позже после проверки, что всё работает:
-- ALTER TABLE score_conditions DROP COLUMN period_type;

View File

@@ -0,0 +1,18 @@
-- Migration: Add wishlist_id to tasks table for linking tasks to wishlist items
-- This allows creating tasks directly from wishlist items and tracking the relationship
-- Добавляем поле wishlist_id в таблицу tasks
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS wishlist_id INTEGER REFERENCES wishlist_items(id) ON DELETE SET NULL;
-- Создаём индекс для быстрого поиска задач по wishlist_id
CREATE INDEX IF NOT EXISTS idx_tasks_wishlist_id ON tasks(wishlist_id);
-- Уникальный индекс: только одна незавершённая задача на желание
-- Это предотвращает создание нескольких задач для одного желания
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_wishlist_id_unique
ON tasks(wishlist_id) WHERE wishlist_id IS NOT NULL AND deleted = FALSE;
-- Добавляем комментарий для документации
COMMENT ON COLUMN tasks.wishlist_id IS 'Link to wishlist item that this task fulfills. NULL if task is not linked to any wishlist item.';

View File

@@ -0,0 +1,49 @@
-- Migration: Refactor configs to link via tasks.config_id
-- This migration adds config_id to tasks table and migrates existing configs to tasks
-- After migration: configs only contain words_count, max_cards (name and try_message removed)
-- ============================================
-- Step 1: Add config_id to tasks
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS config_id INTEGER REFERENCES configs(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_tasks_config_id ON tasks(config_id);
-- Unique index: only one task per config
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_config_id_unique
ON tasks(config_id) WHERE config_id IS NOT NULL AND deleted = FALSE;
COMMENT ON COLUMN tasks.config_id IS 'Link to test config. NULL if task is not a test.';
-- ============================================
-- Step 2: Migrate existing configs to tasks
-- Create a task for each config that doesn't have one yet
-- ============================================
INSERT INTO tasks (user_id, name, reward_message, repetition_period, repetition_date, config_id)
SELECT
c.user_id,
c.name, -- Config name -> Task name
c.try_message, -- try_message -> reward_message
'0 day'::INTERVAL, -- repetition_period = 0 (infinite task)
'0 week', -- repetition_date = 0 (infinite task)
c.id -- Link to config
FROM configs c
WHERE c.name IS NOT NULL -- Only configs with names
AND NOT EXISTS (
SELECT 1 FROM tasks t WHERE t.config_id = c.id AND t.deleted = FALSE
);
-- ============================================
-- Step 3: Remove name and try_message from configs
-- These are now stored in the linked task
-- ============================================
ALTER TABLE configs DROP COLUMN IF EXISTS name;
ALTER TABLE configs DROP COLUMN IF EXISTS try_message;
-- ============================================
-- Comments for documentation
-- ============================================
COMMENT ON TABLE configs IS 'Test configurations (words_count, max_cards, dictionary associations). Linked to tasks via tasks.config_id.';

View File

@@ -0,0 +1,116 @@
-- Migration: Add wishlist boards for multi-user collaboration
-- Each user can have multiple boards, share them via invite links,
-- and collaborate with other users on shared wishes
-- ============================================
-- Table: wishlist_boards (доски желаний)
-- ============================================
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_wishlist_boards_owner_id ON wishlist_boards(owner_id);
CREATE INDEX IF NOT EXISTS idx_wishlist_boards_invite_token ON wishlist_boards(invite_token)
WHERE invite_token IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_wishlist_boards_owner_deleted ON wishlist_boards(owner_id, deleted);
-- ============================================
-- Table: wishlist_board_members (участники доски)
-- ============================================
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_board_members_board_id ON wishlist_board_members(board_id);
CREATE INDEX IF NOT EXISTS idx_board_members_user_id ON wishlist_board_members(user_id);
-- ============================================
-- Modify: wishlist_items - добавляем board_id и author_id
-- ============================================
ALTER TABLE wishlist_items
ADD COLUMN IF NOT EXISTS board_id INTEGER REFERENCES wishlist_boards(id) ON DELETE CASCADE;
ALTER TABLE wishlist_items
ADD COLUMN IF NOT EXISTS author_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_wishlist_items_board_id ON wishlist_items(board_id);
CREATE INDEX IF NOT EXISTS idx_wishlist_items_author_id ON wishlist_items(author_id);
-- ============================================
-- Modify: wishlist_conditions - добавляем user_id для персональных целей
-- ============================================
ALTER TABLE wishlist_conditions
ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_wishlist_conditions_user_id ON wishlist_conditions(user_id);
-- ============================================
-- Modify: tasks - добавляем политику награждения для wishlist задач
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS reward_policy VARCHAR(20) DEFAULT 'personal';
COMMENT ON COLUMN tasks.reward_policy IS
'For wishlist tasks: personal = only if user completes, shared = anyone completes';
-- ============================================
-- Миграция данных: Этап 1 - создаём персональные доски
-- ============================================
-- Создаём доску "Мои желания" для каждого пользователя с желаниями
INSERT INTO wishlist_boards (owner_id, name)
SELECT DISTINCT user_id, 'Мои желания'
FROM wishlist_items
WHERE user_id IS NOT NULL
AND deleted = FALSE
AND NOT EXISTS (
SELECT 1 FROM wishlist_boards wb
WHERE wb.owner_id = wishlist_items.user_id AND wb.name = 'Мои желания'
);
-- ============================================
-- Миграция данных: Этап 2 - привязываем желания к доскам
-- ============================================
UPDATE wishlist_items wi
SET
board_id = wb.id,
author_id = COALESCE(wi.author_id, wi.user_id)
FROM wishlist_boards wb
WHERE wi.board_id IS NULL
AND wi.user_id = wb.owner_id
AND wb.name = 'Мои желания';
-- ============================================
-- Миграция данных: Этап 3 - заполняем user_id в условиях
-- ============================================
UPDATE wishlist_conditions wc
SET user_id = wi.user_id
FROM wishlist_items wi
WHERE wc.wishlist_item_id = wi.id
AND wc.user_id IS NULL;
-- ============================================
-- Comments
-- ============================================
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_conditions.user_id IS 'Owner of this condition. Each user has their own goals on shared boards.';
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';

View File

@@ -0,0 +1,13 @@
-- Migration: Add reward_policy to tasks table
-- This migration adds reward_policy column for wishlist tasks
-- If the column already exists (from migration 023), this will be a no-op
-- ============================================
-- Modify: tasks - добавляем политику награждения для wishlist задач
-- ============================================
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS reward_policy VARCHAR(20) DEFAULT 'personal';
COMMENT ON COLUMN tasks.reward_policy IS
'For wishlist tasks: personal = only if user completes, shared = anyone completes';

View File

@@ -0,0 +1,13 @@
-- Migration: Remove wishlist conditions without user_id
-- These conditions should not exist as every condition must have an owner
-- This migration removes orphaned conditions that were created before the fix
-- ============================================
-- Remove conditions without user_id
-- ============================================
DELETE FROM wishlist_conditions WHERE user_id IS NULL;
-- ============================================
-- Comments
-- ============================================
COMMENT ON COLUMN wishlist_conditions.user_id IS 'Owner of this condition. Each user has their own goals on shared boards. Required field.';

View File

@@ -0,0 +1,10 @@
-- Migration: Add weekly_goals.max_score snapshot column and drop unused actual_score
-- Date: 2026-01-24
ALTER TABLE weekly_goals
DROP COLUMN IF EXISTS actual_score;
-- max_score is a snapshot of max_goal_score for a week, filled only for new weeks by cron
ALTER TABLE weekly_goals
ADD COLUMN IF NOT EXISTS max_score NUMERIC(10,4);

View File

@@ -0,0 +1,51 @@
-- Migration: Add normalized_total_score to weekly_report_mv using weekly_goals.max_score
-- Date: 2026-01-24
--
-- normalized_total_score = LEAST(total_score, max_score) if max_score is set, else total_score.
-- Note: max_score is a snapshot field (filled only for new weeks by cron).
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
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 IF NOT EXISTS 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.';

View File

@@ -0,0 +1,14 @@
-- Migration: Optimize task queries with composite index
-- Date: 2026-01-24
--
-- This migration adds a composite index to optimize the task detail query:
-- WHERE id = $1 AND user_id = $2 AND deleted = FALSE
--
-- The index uses a partial index with WHERE deleted = FALSE to reduce index size
-- and improve query performance for active (non-deleted) tasks.
CREATE INDEX IF NOT EXISTS idx_tasks_id_user_deleted
ON tasks(id, user_id, deleted)
WHERE deleted = FALSE;
COMMENT ON INDEX idx_tasks_id_user_deleted IS 'Composite index for optimizing task detail queries with id, user_id, and deleted filter. Partial index for non-deleted tasks only.';

View File

@@ -0,0 +1,25 @@
-- Migration: Add covering indexes for task detail queries
-- Date: 2026-01-25
--
-- This migration adds covering indexes to optimize queries by including
-- all needed columns in the index, avoiding table lookups.
--
-- Covering indexes allow PostgreSQL to perform index-only scans,
-- getting all data directly from the index without accessing the table.
-- Covering index for subtasks query
-- Includes all columns needed for subtasks selection to avoid table lookups
CREATE INDEX IF NOT EXISTS 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;
-- Covering index for wishlist name lookup
-- Includes name and deleted flag for quick lookup without table access
CREATE INDEX IF NOT EXISTS idx_wishlist_items_id_deleted_covering
ON wishlist_items(id, deleted)
INCLUDE (name)
WHERE deleted = FALSE;
COMMENT ON INDEX idx_tasks_parent_deleted_covering IS 'Covering index for subtasks query - includes all selected columns to avoid table lookups. Enables index-only scans for better performance.';
COMMENT ON INDEX idx_wishlist_items_id_deleted_covering IS 'Covering index for wishlist name lookup - includes name to avoid table lookup. Enables index-only scans for better performance.';

View File

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