package db import ( "context" "database/sql" "fmt" "time" _ "github.com/lib/pq" ) type PostgresDB struct { db *sql.DB } func NewPostgresDB(databaseURL string) (*PostgresDB, error) { db, err := sql.Open("postgres", databaseURL) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } return &PostgresDB{db: db}, nil } func (p *PostgresDB) Close() error { return p.db.Close() } func (p *PostgresDB) DB() *sql.DB { return p.db } func (p *PostgresDB) RunMigrations(ctx context.Context) error { migrations := []string{ `CREATE TABLE IF NOT EXISTS digests ( id SERIAL PRIMARY KEY, topic VARCHAR(100) NOT NULL, region VARCHAR(50) NOT NULL, cluster_title VARCHAR(500) NOT NULL, summary_ru TEXT NOT NULL, citations JSONB DEFAULT '[]', sources_count INT DEFAULT 0, follow_up JSONB DEFAULT '[]', thumbnail TEXT, short_description TEXT, main_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(topic, region, cluster_title) )`, `CREATE INDEX IF NOT EXISTS idx_digests_topic_region ON digests(topic, region)`, `CREATE INDEX IF NOT EXISTS idx_digests_main_url ON digests(main_url)`, `CREATE TABLE IF NOT EXISTS article_summaries ( id SERIAL PRIMARY KEY, url_hash VARCHAR(64) NOT NULL UNIQUE, url TEXT NOT NULL, events JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '7 days' )`, `CREATE INDEX IF NOT EXISTS idx_article_summaries_url_hash ON article_summaries(url_hash)`, `CREATE INDEX IF NOT EXISTS idx_article_summaries_expires ON article_summaries(expires_at)`, `CREATE TABLE IF NOT EXISTS collections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, is_public BOOLEAN DEFAULT FALSE, context_enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, `CREATE INDEX IF NOT EXISTS idx_collections_user ON collections(user_id)`, `CREATE TABLE IF NOT EXISTS collection_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, item_type VARCHAR(50) NOT NULL, title VARCHAR(500), content TEXT, url TEXT, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), sort_order INT DEFAULT 0 )`, `CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_id)`, `CREATE TABLE IF NOT EXISTS uploaded_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, filename VARCHAR(500) NOT NULL, file_type VARCHAR(100) NOT NULL, file_size BIGINT NOT NULL, storage_path TEXT NOT NULL, extracted_text TEXT, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() )`, `CREATE INDEX IF NOT EXISTS idx_uploaded_files_user ON uploaded_files(user_id)`, `CREATE TABLE IF NOT EXISTS research_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID, collection_id UUID REFERENCES collections(id) ON DELETE SET NULL, query TEXT NOT NULL, focus_mode VARCHAR(50) DEFAULT 'all', optimization_mode VARCHAR(50) DEFAULT 'balanced', sources JSONB DEFAULT '[]', response_blocks JSONB DEFAULT '[]', final_answer TEXT, citations JSONB DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT NOW(), completed_at TIMESTAMPTZ )`, `CREATE INDEX IF NOT EXISTS idx_research_sessions_user ON research_sessions(user_id)`, `CREATE INDEX IF NOT EXISTS idx_research_sessions_collection ON research_sessions(collection_id)`, } for _, migration := range migrations { if _, err := p.db.ExecContext(ctx, migration); err != nil { return fmt.Errorf("migration failed: %w", err) } } return nil }