CREATE EXTENSION IF NOT EXISTS postgis; CREATE TABLE athletes ( id BIGSERIAL PRIMARY KEY, intervals_athlete_id TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, access_token TEXT NOT NULL, scopes TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE oauth_states ( state TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE sessions ( token_hash BYTEA PRIMARY KEY, athlete_id BIGINT NOT NULL REFERENCES athletes(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX sessions_athlete_idx ON sessions (athlete_id); CREATE TABLE activities ( id BIGSERIAL PRIMARY KEY, athlete_id BIGINT NOT NULL REFERENCES athletes(id) ON DELETE CASCADE, intervals_activity_id TEXT NOT NULL, start_time TIMESTAMPTZ, processed_at TIMESTAMPTZ, activity_json JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (athlete_id, intervals_activity_id) ); CREATE INDEX activities_start_time_idx ON activities(start_time); CREATE TABLE county_boundaries ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, geometry geometry(MultiPolygon, 4326) NOT NULL ); CREATE INDEX county_boundaries_geometry_idx ON county_boundaries USING GIST (geometry); CREATE TABLE county_crossings ( id BIGSERIAL PRIMARY KEY, activity_id BIGINT NOT NULL REFERENCES activities(id) ON DELETE CASCADE, track_index BIGINT NOT NULL, crossing_time TIMESTAMPTZ(3) NOT NULL, location geometry(Point, 4326) NOT NULL, from_county_id BIGINT NOT NULL REFERENCES county_boundaries(id), to_county_id BIGINT NOT NULL REFERENCES county_boundaries(id), intervals_interval JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX county_crossings_time_idx ON county_crossings(crossing_time); CREATE INDEX county_crossings_location_idx ON county_crossings USING GIST (location); CREATE INDEX county_crossings_direction_time_idx ON county_crossings( from_county_id, to_county_id, crossing_time );