← back to Nationalrealestate

db/migrations/001_init.sql

145 lines

-- USRealEstate canonical schema v1.
-- Regions are the anchor; every metric, score, broker, and listing hangs off region_id.

CREATE TABLE region (
  id SERIAL PRIMARY KEY,
  region_type TEXT NOT NULL CHECK (region_type IN ('county','metro','state','zip')),
  canonical_key TEXT NOT NULL UNIQUE,      -- 'county:06037' | 'metro:31080' | 'state:CA' | 'zip:90210'
  name TEXT NOT NULL,
  state_code TEXT,
  fips TEXT,
  cbsa_code TEXT,
  lat DOUBLE PRECISION,
  lng DOUBLE PRECISION,
  population INTEGER,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_region_type ON region (region_type);
CREATE INDEX idx_region_fips ON region (fips) WHERE fips IS NOT NULL;

-- crosswalk: every source's own region id -> our region (Zillow RegionID, Redfin table_id, ACS geoid)
CREATE TABLE source_region_map (
  source TEXT NOT NULL,
  source_region_id TEXT NOT NULL,
  region_id INT NOT NULL REFERENCES region(id),
  match_method TEXT,                        -- 'fips' | 'cbsa' | 'name' | 'manual'
  PRIMARY KEY (source, source_region_id)
);

CREATE TABLE ingest_runs (
  id SERIAL PRIMARY KEY,
  source TEXT NOT NULL,
  url TEXT,
  file_hash TEXT,
  started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  finished_at TIMESTAMPTZ,
  status TEXT NOT NULL DEFAULT 'running',   -- running|ok|failed
  rows_upserted INT DEFAULT 0,
  rows_skipped INT DEFAULT 0,
  notes TEXT
);

CREATE TABLE metric_series (
  region_id INT NOT NULL REFERENCES region(id),
  source TEXT NOT NULL,                     -- 'redfin'|'zillow'|'acs'|'fhfa'|'derived'
  metric TEXT NOT NULL,                     -- 'median_sale_price','inventory','dom','sale_to_list',
                                            -- 'zhvi','zori','hpi','median_hh_income','median_gross_rent',
                                            -- 'vacancy_rate','affordability','rent_yield', ...
  period DATE NOT NULL,
  value NUMERIC,
  ingest_run_id INT REFERENCES ingest_runs(id),
  PRIMARY KEY (region_id, source, metric, period)
);
CREATE INDEX idx_ms_metric_period ON metric_series (metric, period);

CREATE TABLE region_scores (
  region_id INT NOT NULL REFERENCES region(id),
  score_date DATE NOT NULL,
  opportunity_score NUMERIC NOT NULL,
  components JSONB NOT NULL,                -- {momentum:.., affordability:.., rent_yield:.., inventory_trend:.., velocity:.., data_completeness:.., weights:{...}}
  rank INT,
  PRIMARY KEY (region_id, score_date)
);

CREATE TABLE watchlist (
  id SERIAL PRIMARY KEY,
  region_id INT NOT NULL REFERENCES region(id),
  label TEXT,
  thresholds JSONB,                         -- {metric:'zhvi_yoy', op:'>', value:0.08}
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE alerts_log (
  id SERIAL PRIMARY KEY,
  watchlist_id INT REFERENCES watchlist(id),
  fired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  metric TEXT,
  observed NUMERIC,
  detail JSONB
);

-- ── Broker track (Steve 2026-07-21): all US brokers/brokerages, national CRCP model ──
CREATE TABLE firm (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  normalized_name TEXT,                     -- lower/stripped for dedupe across states
  website TEXT,
  phone TEXT,
  hq_city TEXT,
  hq_state TEXT,
  license_no TEXT,
  license_state TEXT,
  source TEXT NOT NULL,                     -- 'ca_dre'|'tx_trec'|'fl_dbpr'|'ny_dos'|...
  source_id TEXT NOT NULL,
  agent_count INT,
  region_id INT REFERENCES region(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (source, source_id)
);
CREATE INDEX idx_firm_norm ON firm (normalized_name);
CREATE INDEX idx_firm_state ON firm (license_state);

CREATE TABLE broker (
  id SERIAL PRIMARY KEY,
  firm_id INT REFERENCES firm(id),
  name TEXT NOT NULL,
  license_no TEXT,
  license_state TEXT,
  license_type TEXT,                        -- broker | salesperson | associate-broker (per state vocab)
  license_status TEXT,                      -- active | expired | suspended ...
  email TEXT,
  phone TEXT,
  city TEXT,
  state_code TEXT,
  source TEXT NOT NULL,
  source_id TEXT NOT NULL,
  region_id INT REFERENCES region(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (source, source_id)
);
CREATE INDEX idx_broker_firm ON broker (firm_id);
CREATE INDEX idx_broker_state ON broker (license_state);

-- pluggable listings slot: fills from broker-site pilots (M-B3) / future licensed feeds
CREATE TABLE listing (
  id SERIAL PRIMARY KEY,
  region_id INT REFERENCES region(id),
  firm_id INT REFERENCES firm(id),
  broker_id INT REFERENCES broker(id),
  source TEXT NOT NULL,
  source_id TEXT NOT NULL,
  address TEXT,
  price NUMERIC,
  beds NUMERIC,
  baths NUMERIC,
  sqft NUMERIC,
  lat DOUBLE PRECISION,
  lng DOUBLE PRECISION,
  url TEXT,
  status TEXT,
  first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_seen TIMESTAMPTZ,
  UNIQUE (source, source_id)
);
CREATE INDEX idx_listing_region ON listing (region_id);