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:
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);
|
||||
@@ -1,106 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
-- 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 (Все слова)';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
-- 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;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
-- 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)';
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
-- 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).
|
||||
@@ -1,17 +0,0 @@
|
||||
-- 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})';
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
-- 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 при первом подключении';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
-- 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)';
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 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)';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 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)';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
-- 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;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- 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';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- 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.';
|
||||
@@ -1,10 +0,0 @@
|
||||
-- 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);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
-- 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.';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 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.';
|
||||
@@ -1,25 +0,0 @@
|
||||
-- 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.';
|
||||
Reference in New Issue
Block a user