CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name VARCHAR(100), avatar_path TEXT, instagram_url VARCHAR(500), profile_visibility VARCHAR(20) NOT NULL DEFAULT 'friends' CHECK (profile_visibility IN ('public', 'friends', 'nobody')), is_admin BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE account_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, purpose VARCHAR(30) NOT NULL CHECK (purpose = 'password_reset'), token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_account_tokens_lookup ON account_tokens (token_hash, purpose, expires_at); CREATE TABLE friendships ( id SERIAL PRIMARY KEY, requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, addressee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted')), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CHECK (requester_id <> addressee_id), UNIQUE (requester_id, addressee_id) ); CREATE UNIQUE INDEX idx_friendships_pair ON friendships (LEAST(requester_id, addressee_id), GREATEST(requester_id, addressee_id)); CREATE TABLE direct_messages ( id SERIAL PRIMARY KEY, sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, body TEXT NOT NULL, read_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CHECK (sender_id <> recipient_id) ); CREATE INDEX idx_direct_messages_conversation ON direct_messages (sender_id, recipient_id, created_at); CREATE INDEX idx_direct_messages_unread ON direct_messages (recipient_id, read_at); CREATE TABLE registration_invites ( id SERIAL PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMP, used_by INTEGER REFERENCES users(id), used_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sessions ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE venues ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, street VARCHAR(255), postal_code VARCHAR(20), city VARCHAR(100), country VARCHAR(100) DEFAULT 'Deutschland', latitude DOUBLE PRECISION, longitude DOUBLE PRECISION, external_id VARCHAR(255), source VARCHAR(50), is_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE venue_aliases ( id SERIAL PRIMARY KEY, venue_id INTEGER NOT NULL REFERENCES venues(id) ON DELETE CASCADE, alias VARCHAR(255) NOT NULL, UNIQUE (venue_id, alias) ); CREATE TABLE concerts ( id SERIAL PRIMARY KEY, artist VARCHAR(255) NOT NULL, event_type VARCHAR(20) NOT NULL DEFAULT 'concert' CHECK (event_type IN ('concert', 'festival', 'other')), parent_event_id INTEGER REFERENCES concerts(id) ON DELETE SET NULL, venue_id INTEGER REFERENCES venues(id) ON DELETE SET NULL, start_datetime TIMESTAMP NOT NULL, end_datetime TIMESTAMP, description TEXT, ticket_url TEXT, ticket_price NUMERIC(10,2), flyer_path TEXT, flyer_url TEXT, created_by INTEGER REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE concert_comments ( id SERIAL PRIMARY KEY, concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, body TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE concert_photos ( id SERIAL PRIMARY KEY, concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, path TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE concert_attendance ( concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL CHECK ( status IN ('attending', 'maybe', 'ticket_search', 'ticket_offer') ), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (concert_id, user_id) ); CREATE TABLE user_badges ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, badge_code VARCHAR(50) NOT NULL, awarded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, badge_code) ); CREATE INDEX idx_concerts_start_datetime ON concerts(start_datetime); CREATE INDEX idx_venues_name ON venues(name); CREATE INDEX idx_venues_external_id ON venues(external_id); CREATE INDEX idx_venue_aliases_alias ON venue_aliases (LOWER(alias)); CREATE INDEX idx_concerts_parent_event_id ON concerts(parent_event_id); CREATE INDEX idx_concert_comments_concert ON concert_comments(concert_id, created_at); CREATE INDEX idx_concert_photos_concert ON concert_photos(concert_id, created_at); CREATE INDEX idx_concert_attendance_concert ON concert_attendance(concert_id, status);