guidly/db/migrations/001_initial.sql
laruevin d5ed7fdcf9 Initial commit: Guidly project with CI/CD pipeline
Telegram Bot + Mini App for city walking quests.
- React 19 + TypeScript + Vite 6 frontend
- Express 5 + PostgreSQL backend
- grammY Telegram bot with DeepSeek AI
- GitLab CI/CD: lint, build, deploy to production

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:42:42 +07:00

171 lines
6.5 KiB
SQL

-- Guidly: Initial database schema
-- Run with: npm run migrate
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================
-- Cities
-- ============================================
CREATE TABLE cities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
country VARCHAR(100) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'inactive')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- Users
-- ============================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
telegram_id BIGINT NOT NULL UNIQUE,
username VARCHAR(255),
display_name VARCHAR(255),
language_code VARCHAR(10) DEFAULT 'ru',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_telegram_id ON users(telegram_id);
-- ============================================
-- Quests
-- ============================================
CREATE TABLE quests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
city_id UUID NOT NULL REFERENCES cities(id),
user_id UUID NOT NULL REFERENCES users(id),
title TEXT,
description TEXT,
number_of_days INT NOT NULL,
pace VARCHAR(20) NOT NULL CHECK (pace IN ('slow', 'normal', 'active')),
companions VARCHAR(20) NOT NULL CHECK (companions IN ('solo', 'couple', 'family', 'friends')),
user_comment TEXT,
status VARCHAR(30) NOT NULL DEFAULT 'not_started'
CHECK (status IN ('not_started', 'in_progress', 'completed')),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quests_user_id ON quests(user_id);
CREATE INDEX idx_quests_user_status ON quests(user_id, status);
-- ============================================
-- Quest Days
-- ============================================
CREATE TABLE quest_days (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
quest_id UUID NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
day_number INT NOT NULL,
theme TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'not_started'
CHECK (status IN ('not_started', 'in_progress', 'completed')),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (quest_id, day_number)
);
CREATE INDEX idx_quest_days_quest ON quest_days(quest_id);
-- ============================================
-- Quest Points (individual stops/locations)
-- ============================================
CREATE TABLE quest_points (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
day_id UUID NOT NULL REFERENCES quest_days(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
location_lat DECIMAL(10, 7),
location_lon DECIMAL(10, 7),
teaser_text TEXT,
content_text TEXT,
order_in_day INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'hidden'
CHECK (status IN ('hidden', 'preview', 'active', 'completed', 'skipped')),
arrived_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (day_id, order_in_day)
);
CREATE INDEX idx_quest_points_day ON quest_points(day_id);
-- ============================================
-- Quest Events (Mini App -> Bot communication)
-- ============================================
CREATE TABLE quest_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
quest_id UUID NOT NULL REFERENCES quests(id),
user_id UUID NOT NULL REFERENCES users(id),
point_id UUID REFERENCES quest_points(id),
event_type VARCHAR(30) NOT NULL
CHECK (event_type IN (
'arrived', 'photo_uploaded', 'skipped',
'finished_today', 'point_completed',
'quest_started', 'quest_completed',
'day_started', 'day_completed',
'peek_next'
)),
payload JSONB DEFAULT '{}',
processed BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quest_events_quest ON quest_events(quest_id);
CREATE INDEX idx_quest_events_unprocessed ON quest_events(processed) WHERE processed = false;
-- ============================================
-- Chat History
-- ============================================
CREATE TABLE chat_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
quest_id UUID NOT NULL REFERENCES quests(id),
point_id UUID REFERENCES quest_points(id),
message_type VARCHAR(20) NOT NULL
CHECK (message_type IN ('user_text', 'user_voice', 'ai_text', 'ai_audio')),
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_chat_history_quest ON chat_history(quest_id);
-- ============================================
-- Achievements
-- ============================================
CREATE TABLE achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
type VARCHAR(50) NOT NULL
CHECK (type IN (
'first_quest', 'quest_completed',
'cities_2', 'cities_5', 'cities_10',
'steps_50', 'steps_100'
)),
city_id UUID REFERENCES cities(id),
achieved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, type, city_id)
);
CREATE INDEX idx_achievements_user ON achievements(user_id);
-- ============================================
-- Seed: Test cities
-- ============================================
INSERT INTO cities (name, country, description) VALUES
('Novosibirsk', 'Russia', 'The cultural and scientific capital of Siberia'),
('Moscow', 'Russia', 'The heart of Russia with centuries of history'),
('Saint Petersburg', 'Russia', 'The cultural capital with imperial grandeur'),
('Istanbul', 'Turkey', 'Where East meets West across the Bosphorus'),
('Barcelona', 'Spain', 'A city of art, architecture and Mediterranean spirit');