CREATE SCHEMA IF NOT EXISTS raw; CREATE SCHEMA IF NOT EXISTS bronze; CREATE SCHEMA IF NOT EXISTS silver; CREATE SCHEMA IF NOT EXISTS gold; CREATE SCHEMA IF NOT EXISTS app; CREATE SCHEMA IF NOT EXISTS audit; CREATE TABLE IF NOT EXISTS audit.data_sources ( source_id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, source_type TEXT NOT NULL, url TEXT, cadence TEXT, reliability TEXT NOT NULL DEFAULT 'unknown', notes TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS audit.ingestion_runs ( run_id BIGSERIAL PRIMARY KEY, source_id BIGINT REFERENCES audit.data_sources(source_id), status TEXT NOT NULL, started_at TIMESTAMPTZ NOT NULL DEFAULT now(), finished_at TIMESTAMPTZ, raw_uri TEXT, row_count INTEGER, error_message TEXT ); CREATE TABLE IF NOT EXISTS silver.areas ( area_id TEXT PRIMARY KEY, name TEXT NOT NULL, district TEXT NOT NULL, segment TEXT NOT NULL, notes TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS silver.area_monthly_metrics ( area_id TEXT NOT NULL REFERENCES silver.areas(area_id), month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'), transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0), transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0), listing_count INTEGER NOT NULL CHECK (listing_count >= 0), listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0), median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0), rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0), new_supply_units INTEGER NOT NULL CHECK (new_supply_units >= 0), land_residential_gfa_sqm DOUBLE PRECISION NOT NULL CHECK (land_residential_gfa_sqm >= 0), mortgage_rate_pct DOUBLE PRECISION NOT NULL CHECK (mortgage_rate_pct >= 0), policy_signal INTEGER NOT NULL DEFAULT 0 CHECK (policy_signal BETWEEN -2 AND 2), source TEXT NOT NULL DEFAULT 'manual', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (area_id, month) ); CREATE TABLE IF NOT EXISTS silver.neighborhoods ( neighborhood_id TEXT PRIMARY KEY, area_id TEXT NOT NULL REFERENCES silver.areas(area_id), name TEXT NOT NULL, built_year INTEGER, property_type TEXT NOT NULL, metro_distance_m INTEGER, school_quality TEXT, notes TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS silver.neighborhood_monthly_metrics ( neighborhood_id TEXT NOT NULL REFERENCES silver.neighborhoods(neighborhood_id), month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'), transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0), transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0), listing_count INTEGER NOT NULL CHECK (listing_count >= 0), listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0), rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0), median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0), available_units INTEGER NOT NULL CHECK (available_units >= 0), source TEXT NOT NULL DEFAULT 'manual', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (neighborhood_id, month) ); CREATE TABLE IF NOT EXISTS gold.area_scores ( area_id TEXT NOT NULL REFERENCES silver.areas(area_id), name TEXT NOT NULL, district TEXT NOT NULL, segment TEXT NOT NULL, month TEXT NOT NULL CHECK (month ~ '^[0-9]{4}-[0-9]{2}$'), investment_score DOUBLE PRECISION NOT NULL CHECK (investment_score BETWEEN 0 AND 100), recommendation TEXT NOT NULL, liquidity_score DOUBLE PRECISION NOT NULL CHECK (liquidity_score BETWEEN 0 AND 100), momentum_score DOUBLE PRECISION NOT NULL CHECK (momentum_score BETWEEN 0 AND 100), rent_support_score DOUBLE PRECISION NOT NULL CHECK (rent_support_score BETWEEN 0 AND 100), safety_margin_score DOUBLE PRECISION NOT NULL CHECK (safety_margin_score BETWEEN 0 AND 100), credit_support_score DOUBLE PRECISION NOT NULL CHECK (credit_support_score BETWEEN 0 AND 100), supply_risk_score DOUBLE PRECISION NOT NULL CHECK (supply_risk_score BETWEEN 0 AND 100), valuation_pressure_score DOUBLE PRECISION NOT NULL CHECK (valuation_pressure_score BETWEEN 0 AND 100), transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0), transaction_price_psm DOUBLE PRECISION NOT NULL CHECK (transaction_price_psm >= 0), listing_count INTEGER NOT NULL CHECK (listing_count >= 0), listing_price_psm DOUBLE PRECISION NOT NULL CHECK (listing_price_psm >= 0), rent_price_psm DOUBLE PRECISION NOT NULL CHECK (rent_price_psm >= 0), median_days_on_market DOUBLE PRECISION NOT NULL CHECK (median_days_on_market >= 0), annual_rent_yield_pct DOUBLE PRECISION NOT NULL CHECK (annual_rent_yield_pct >= 0), listing_pressure_ratio DOUBLE PRECISION NOT NULL CHECK (listing_pressure_ratio >= 0), discount_pct DOUBLE PRECISION NOT NULL, price_momentum_pct DOUBLE PRECISION NOT NULL, volume_momentum_pct DOUBLE PRECISION NOT NULL, model_version TEXT NOT NULL DEFAULT 'sample-v1', computed_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (area_id, month) ); CREATE TABLE IF NOT EXISTS app.watchlist_items ( watchlist_item_id BIGSERIAL PRIMARY KEY, neighborhood_id TEXT REFERENCES silver.neighborhoods(neighborhood_id), area_id TEXT REFERENCES silver.areas(area_id), target_price_psm DOUBLE PRECISION CHECK (target_price_psm >= 0), status TEXT NOT NULL DEFAULT 'active', notes TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (neighborhood_id IS NOT NULL OR area_id IS NOT NULL) ); CREATE INDEX IF NOT EXISTS idx_area_monthly_metrics_month ON silver.area_monthly_metrics(month); CREATE INDEX IF NOT EXISTS idx_neighborhoods_area ON silver.neighborhoods(area_id); CREATE INDEX IF NOT EXISTS idx_gold_area_scores_month ON gold.area_scores(month, investment_score DESC); CREATE INDEX IF NOT EXISTS idx_watchlist_status ON app.watchlist_items(status);