create extension if not exists pgcrypto; create extension if not exists postgis; create or replace function public.set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; create table if not exists public.users ( id uuid primary key default gen_random_uuid(), email text not null unique, password_hash text not null, display_name text, avatar_url text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.sessions ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users (id) on delete cascade, token_hash text not null unique, expires_at timestamptz not null, last_seen_at timestamptz, user_agent text, ip_address inet, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.postal_areas ( id uuid primary key default gen_random_uuid(), country_code text not null, postal_code text not null, display_name text, normalized_postal_code text not null, geom geometry(multipolygon, 4326) not null, centroid geography(point, 4326), search_radius_m integer, metadata_json jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint postal_areas_country_code_postal_code_key unique (country_code, normalized_postal_code) ); create table if not exists public.deep_research_batches ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users (id) on delete cascade, pin_lat double precision not null, pin_lng double precision not null, pin_geom geography(point, 4326), base_postal_code text, country_code text, propagation integer not null default 0, business_type text not null, keywords text, status text not null check (status in ('pending', 'running', 'completed', 'failed', 'stopped')), total_postal_areas integer not null default 0, total_results integer not null default 0, started_at timestamptz, completed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.search_jobs ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users (id) on delete cascade, deep_research_batch_id uuid references public.deep_research_batches (id) on delete set null, postal_area_id uuid references public.postal_areas (id) on delete set null, name text not null, city text, address text, postal_code text, country_code text, radius_km numeric not null, business_type text not null, keywords text, status text not null check (status in ('pending', 'running', 'completed', 'failed', 'stopped')), total_results integer not null default 0, cancel_requested boolean not null default false, requested_lead_limit integer not null default 60, search_center_geom geography(point, 4326), started_at timestamptz, completed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists public.businesses ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users (id) on delete cascade, external_source_id text, source text not null, name text not null, address text, city text, state_province text, postal_code text, country text, phone text, website text, rating numeric, review_count integer, category text, hours_json jsonb, latitude double precision, longitude double precision, geom geography(point, 4326), general_info text, metadata_json jsonb, first_seen_at timestamptz, last_seen_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint businesses_user_source_external_source_key unique (user_id, source, external_source_id) ); create table if not exists public.search_job_results ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users (id) on delete cascade, search_job_id uuid not null references public.search_jobs (id) on delete cascade, business_id uuid not null references public.businesses (id) on delete cascade, matched_keywords text[], rank integer, captured_at timestamptz not null default now(), constraint search_job_results_job_business_key unique (search_job_id, business_id) ); create table if not exists public.postal_area_neighbors ( postal_area_id uuid not null references public.postal_areas (id) on delete cascade, neighbor_postal_area_id uuid not null references public.postal_areas (id) on delete cascade, created_at timestamptz not null default now(), primary key (postal_area_id, neighbor_postal_area_id), check (postal_area_id <> neighbor_postal_area_id) ); create index if not exists sessions_user_id_idx on public.sessions (user_id); create index if not exists sessions_expires_at_idx on public.sessions (expires_at); create index if not exists search_jobs_user_created_at_idx on public.search_jobs (user_id, created_at desc); create index if not exists search_jobs_batch_idx on public.search_jobs (deep_research_batch_id); create index if not exists businesses_user_created_at_idx on public.businesses (user_id, created_at desc); create index if not exists search_job_results_user_job_idx on public.search_job_results (user_id, search_job_id); create index if not exists deep_research_batches_user_created_at_idx on public.deep_research_batches (user_id, created_at desc); create index if not exists postal_areas_geom_idx on public.postal_areas using gist (geom); create index if not exists postal_areas_centroid_idx on public.postal_areas using gist (centroid); create index if not exists businesses_geom_idx on public.businesses using gist (geom); drop trigger if exists set_users_updated_at on public.users; create trigger set_users_updated_at before update on public.users for each row execute function public.set_updated_at(); drop trigger if exists set_sessions_updated_at on public.sessions; create trigger set_sessions_updated_at before update on public.sessions for each row execute function public.set_updated_at(); drop trigger if exists set_postal_areas_updated_at on public.postal_areas; create trigger set_postal_areas_updated_at before update on public.postal_areas for each row execute function public.set_updated_at(); drop trigger if exists set_deep_research_batches_updated_at on public.deep_research_batches; create trigger set_deep_research_batches_updated_at before update on public.deep_research_batches for each row execute function public.set_updated_at(); drop trigger if exists set_search_jobs_updated_at on public.search_jobs; create trigger set_search_jobs_updated_at before update on public.search_jobs for each row execute function public.set_updated_at(); drop trigger if exists set_businesses_updated_at on public.businesses; create trigger set_businesses_updated_at before update on public.businesses for each row execute function public.set_updated_at();