CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE TABLE IF NOT EXISTS source ( id TEXT PRIMARY KEY, name TEXT NOT NULL, tier SMALLINT NOT NULL DEFAULT 1, region_code CHAR(6), collector_type TEXT NOT NULL DEFAULT 'python_script', auth_mode TEXT NOT NULL DEFAULT 'none', auth_status TEXT NOT NULL DEFAULT 'none', health TEXT NOT NULL DEFAULT 'ok', last_ingest_at TIMESTAMPTZ, last_count INT, fail_streak INT NOT NULL DEFAULT 0, config JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS raw_page ( sha256 CHAR(64) PRIMARY KEY, url TEXT NOT NULL, status INT, content_type TEXT, fetched_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS announcement ( id BIGSERIAL PRIMARY KEY, source_id TEXT REFERENCES source(id), url TEXT NOT NULL UNIQUE, url_hash CHAR(16) NOT NULL, content_hash CHAR(16), simhash BIGINT, canonical_id BIGINT REFERENCES announcement(id), title TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'job_posting', employer TEXT, employer_type TEXT, province_code CHAR(6), city_code CHAR(6), county_code CHAR(6), published_at DATE, apply_start TIMESTAMPTZ, apply_end TIMESTAMPTZ, body_text TEXT, raw_sha256 CHAR(64) REFERENCES raw_page(sha256), first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_announcement_kind_published ON announcement (kind, published_at DESC); CREATE INDEX IF NOT EXISTS idx_announcement_city_apply_end ON announcement (city_code, apply_end); CREATE INDEX IF NOT EXISTS idx_announcement_title_trgm ON announcement USING gin (title gin_trgm_ops); CREATE TABLE IF NOT EXISTS attachment ( id BIGSERIAL PRIMARY KEY, announcement_id BIGINT NOT NULL REFERENCES announcement(id) ON DELETE CASCADE, url TEXT NOT NULL, filename TEXT, mime TEXT, sha256 CHAR(64), parse_status TEXT NOT NULL DEFAULT 'pending', parser TEXT, error TEXT ); CREATE TABLE IF NOT EXISTS position ( id BIGSERIAL PRIMARY KEY, announcement_id BIGINT NOT NULL REFERENCES announcement(id) ON DELETE CASCADE, attachment_id BIGINT REFERENCES attachment(id), post_code TEXT, dept TEXT, post_name TEXT NOT NULL, headcount INT, education TEXT, edu_min SMALLINT, degree_required BOOLEAN, majors_raw TEXT, majors TEXT[], major_unlimited BOOLEAN NOT NULL DEFAULT false, age_max SMALLINT, bianzhi TEXT, salary_min INT, salary_max INT, work_city_code CHAR(6), other_req TEXT, extract_method TEXT NOT NULL DEFAULT 'edge_python', confidence REAL, raw JSONB ); CREATE INDEX IF NOT EXISTS idx_position_city_edu_age ON position (work_city_code, edu_min, age_max); CREATE INDEX IF NOT EXISTS idx_position_majors ON position USING gin (majors); CREATE TABLE IF NOT EXISTS embedding ( owner_type TEXT NOT NULL, owner_id BIGINT NOT NULL, model TEXT NOT NULL, chunk SMALLINT NOT NULL DEFAULT 0, text_hash CHAR(16) NOT NULL, vec vector(1024) NOT NULL, PRIMARY KEY (owner_type, owner_id, model, chunk) ); CREATE INDEX IF NOT EXISTS idx_embedding_vec_hnsw ON embedding USING hnsw (vec vector_cosine_ops); CREATE TABLE IF NOT EXISTS notify_sent ( channel TEXT NOT NULL, owner_type TEXT NOT NULL, owner_id BIGINT NOT NULL, sent_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel, owner_type, owner_id) );