-- ============================================================ -- willhaben-tracker — initial schema migration -- ============================================================ -- ----------------------------------------------------------- -- 1. users (whitelisted Telegram users) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), telegram_id bigint UNIQUE NOT NULL, username text, first_name text, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------------------- -- 2. search_queries (saved searches per user) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS search_queries ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES users(id) ON DELETE CASCADE NOT NULL, keyword text NOT NULL, interval_minutes int NOT NULL DEFAULT 60, is_active boolean NOT NULL DEFAULT true, last_scraped_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------------------- -- 3. ads (raw ad snapshots, globally deduplicated by wh_ad_id) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS ads ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), wh_ad_id text UNIQUE NOT NULL, raw_json jsonb NOT NULL, title text NOT NULL, price numeric, location text, url text, published_at timestamptz, first_seen_at timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------------------- -- 4. query_ads (junction: which query found which ad) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS query_ads ( search_query_id uuid REFERENCES search_queries(id) ON DELETE CASCADE NOT NULL, ad_id uuid REFERENCES ads(id) ON DELETE CASCADE NOT NULL, first_seen_at timestamptz NOT NULL DEFAULT now(), is_notified boolean NOT NULL DEFAULT false, PRIMARY KEY (search_query_id, ad_id) ); -- ----------------------------------------------------------- -- 5. notifications (audit log of sent Telegram messages) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS notifications ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES users(id) ON DELETE CASCADE NOT NULL, ad_id uuid REFERENCES ads(id) ON DELETE SET NULL, message_id int, sent_at timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------------------- -- 6. scrape_logs (worker health / debugging) -- ----------------------------------------------------------- CREATE TABLE IF NOT EXISTS scrape_logs ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), search_query_id uuid REFERENCES search_queries(id) ON DELETE CASCADE NOT NULL, status text NOT NULL CHECK (status IN ('success', 'error', 'rate_limited')), ads_found int NOT NULL DEFAULT 0, new_ads int NOT NULL DEFAULT 0, error_message text, scraped_at timestamptz NOT NULL DEFAULT now() ); -- ============================================================ -- Indexes -- ============================================================ -- Users: fast lookup by telegram_id (unique constraint already implies an index) -- Search queries: user membership lookups CREATE INDEX IF NOT EXISTS idx_search_queries_user_id ON search_queries(user_id); -- Scheduler polling: active queries ordered by last scraped time CREATE INDEX IF NOT EXISTS idx_search_queries_active_scraped ON search_queries(is_active, last_scraped_at) WHERE is_active = true; -- Notifier lookups: un-notified ads per query CREATE INDEX IF NOT EXISTS idx_query_ads_notified ON query_ads(search_query_id, is_notified) WHERE is_notified = false; -- Notifications: recent messages per user CREATE INDEX IF NOT EXISTS idx_notifications_user_sent ON notifications(user_id, sent_at DESC); -- Scrape logs: latest runs per query CREATE INDEX IF NOT EXISTS idx_scrape_logs_query_at ON scrape_logs(search_query_id, scraped_at DESC);