← back to Stayclaim
db/migrations/006_place_first_satellites.sql
90 lines
-- Place-first satellite tables per docs/deep-research-report.md.
-- The `listing` table remains the canonical place record root (its `id` is
-- the de-facto place_id). These satellites add the dimensions the report
-- mandates without renaming/restructuring existing app surfaces.
-- 1. privacy_class on every place record
ALTER TABLE listing
ADD COLUMN IF NOT EXISTS privacy_class text NOT NULL DEFAULT 'public_history_only';
-- 2. Alternate addresses (one place can have many address strings over time)
CREATE TABLE IF NOT EXISTS address_alt (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
listing_id uuid NOT NULL REFERENCES listing(id) ON DELETE CASCADE,
full_address text NOT NULL, -- "30874 PCH, Malibu CA 90265"
valid_from date,
valid_to date,
canonical boolean NOT NULL DEFAULT false, -- exactly one canonical at a time, app-enforced
source_tier char(1) NOT NULL CHECK (source_tier IN ('A','B','C','D')) DEFAULT 'A',
source_label text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_address_alt_listing ON address_alt (listing_id);
-- 3. Media assets — Sanborn scans, archival photos, drone imagery, postcards
CREATE TABLE IF NOT EXISTS media_asset (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
listing_id uuid NOT NULL REFERENCES listing(id) ON DELETE CASCADE,
kind text NOT NULL, -- archival_photo | historic_map | postcard | drone | streetview_thumb | newspaper_clip
year int, -- year of the asset; nullable when undated
caption text,
storage_url text NOT NULL, -- where the bytes live (S3/R2/object store)
source_tier char(1) NOT NULL CHECK (source_tier IN ('A','B','C','D')) DEFAULT 'B',
source_label text,
source_url text,
rights text, -- "Beverly Hills Public Library — research use only", "PD", "CC-BY-SA-4.0", etc.
added_at timestamptz NOT NULL DEFAULT now(),
review_status text NOT NULL DEFAULT 'approved', -- pending | approved | rejected
public_visible boolean NOT NULL DEFAULT true
);
CREATE INDEX IF NOT EXISTS idx_media_listing ON media_asset (listing_id);
CREATE INDEX IF NOT EXISTS idx_media_year ON media_asset (year);
CREATE INDEX IF NOT EXISTS idx_media_visible ON media_asset (public_visible) WHERE public_visible;
-- 4. Parcels with versioning. Geometry as GeoJSON text until PostGIS lands (pg17).
CREATE TABLE IF NOT EXISTS parcel (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
apn text UNIQUE NOT NULL, -- e.g. "4349-020-012"
jurisdiction text NOT NULL DEFAULT 'Los Angeles County',
current_listing_id uuid REFERENCES listing(id), -- the place this parcel currently maps to
notes text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_parcel_listing ON parcel (current_listing_id);
CREATE TABLE IF NOT EXISTS parcel_version (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
parcel_id uuid NOT NULL REFERENCES parcel(id) ON DELETE CASCADE,
valid_from date NOT NULL,
valid_to date, -- NULL = current
geometry_geojson jsonb, -- GeoJSON polygon; will move to PostGIS geometry later
area_sqft numeric(12,2),
parent_versions uuid[], -- for split/merge lineage
source_tier char(1) NOT NULL CHECK (source_tier IN ('A','B','C','D')) DEFAULT 'A',
source_label text,
recorded_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_parcel_version_parcel ON parcel_version (parcel_id);
CREATE INDEX IF NOT EXISTS idx_parcel_version_validfrom ON parcel_version (valid_from);
-- 5. Structure — one place can have multiple structures across time
-- (original 1928 build → 1972 teardown → 1973 rebuild = three rows)
CREATE TABLE IF NOT EXISTS structure (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
listing_id uuid NOT NULL REFERENCES listing(id) ON DELETE CASCADE,
year_built int,
year_demolished int,
property_type text, -- single_family | duplex | apartment | commercial | mixed_use
style text, -- spanish_colonial | tudor_revival | mid_century | etc.
bedrooms int,
bathrooms numeric(3,1),
building_area_sqft numeric(10,2),
lot_size_sqft numeric(12,2),
notes text,
source_tier char(1) NOT NULL CHECK (source_tier IN ('A','B','C','D')) DEFAULT 'A',
source_label text,
recorded_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_structure_listing ON structure (listing_id);
CREATE INDEX IF NOT EXISTS idx_structure_year_built ON structure (year_built);