Live data¶
Live events live in pogo_live. You read views, not base tables. Each
view calls cuno.check_sub_access; staff set that from your subscription.
Which view to use¶
Prefer sub_v2_* for new work. Those columns match the live table.
The older views sub_spawns, sub_raids, and sub_quests keep a joined
historical shape: raids and quests already include the gym or pokestop
name and point. pogo_live.sub_* is the canonical name; some logins also
have unqualified public.sub_spawns and public.sub_raids.
| View | Dataset | Row |
|---|---|---|
pogo_live.sub_v2_spawns |
spawns |
Spawn |
pogo_live.sub_v2_raids |
raids |
Raid (gym_id) |
pogo_live.sub_v2_quests |
quests |
Quest (pokestop_id) |
pogo_live.sub_v2_quest_rewards |
quests |
Reward slot |
pogo_live.sub_v2_gyms |
gyms |
Gym |
pogo_live.sub_v2_pokestops |
pokestops |
Pokestop |
pogo_live.sub_v2_invasions |
invasions |
Invasion (pokestop_id) |
pogo_live.sub_v2_invasions_lineup |
invasions |
Grunt slot |
pogo_live.sub_v2_power_spots |
power_spots |
Window (station_id, start_time) |
pogo_live.sub_v2_stations |
power_spots |
Power Spot POI |
pogo_live.sub_v2_max_battles |
max_battles |
Fight (bread_battle_seed, station_id) |
pogo_live.sub_v2_max_battle_events |
max_battles |
Current hour per station |
pogo_live.sub_v2_weather |
weather |
Weather cell |
Geography¶
lat/lon numeric(11,7), location geography(Point, 4326).
ST_DWithin is meters, SRID 4326. Longitude first in ST_MakePoint:
ST_DWithin(s.location, ST_MakePoint(lon, lat)::geography, 1000)
Time and timezones¶
All timestamps are timestamptz. Filter on the end column for currently
live rows:
| View | End column |
|---|---|
| Spawns | expires |
| Raids | raid_end |
| Quests | expires |
| Invasions | expiration |
| Power spots | end_time |
| Max battles | battle_end |
The point tables also have timezone_id and timezone_revision.
timezone_revision = 0 means no match (timezone_id NULL); a positive
revision is a completed match. geo.timezone.tz_name is IANA:
SELECT s.spawn_id, s.expires, tz.tz_name,
s.expires AT TIME ZONE tz.tz_name AS expires_local
FROM pogo_live.sub_v2_spawns AS s
JOIN geo.timezone AS tz USING (timezone_id)
WHERE s.expires > now();
geo.timezone_slice isn't granted. Catalog: geo.timezone_release /
geo.catalog_publication (catalog = 'timezone').
Receipt: first_added, first_seen, last_received, modified
(game-state change; can stay null), update_count.
Spawns (sub_v2_spawns)¶
PK spawn_id (snowflake). encounter_id is unique in practice.
Partitioned by spawn_id. Indexes: GiST location, expires,
(pokemon_id, form_id, IVs), (stat_pool_hash, iv_key) where hash
isn't null. Geography + time: both predicates in one WHERE.
WOF columns: Places (WOF). Gender is 1/2/3 (male/female/genderless). Only full encounters land: CP ≥ 10, IVs 0–15 (zero is valid); map sightings without encounter data are dropped.
Legacy sub_spawns omits hashes, form_group_id, iv_key, and some
receipt columns.
Raids (sub_v2_raids)¶
PK raid_id. Times are raid_spawn, raid_start, raid_battle, and
raid_end (indexed on raid_end and on (raid_pokemon_id,
raid_pokemon_form_id)). Boss columns use the raid_pokemon_ prefix.
is_hatched means a mapper reported the boss. Crossing raid_start does
not flip it; an egg can remain the last observed state until another
payload arrives.
An egg is NULL in every boss column (form, costume, CP, moves, gender, temporary evolution, form group). A reported boss has species, form, and positive CP; optional attachments may still be NULL. Event times and tier don't depend on boss presence.
Legacy sub_raids already includes gym name, point, and team.
Zero and NULL¶
| Attribute | Canonical meaning |
|---|---|
| Form 0 | Default form on a present Pokémon; no boss means NULL form. |
| Costume, disguise, temporary evolution, background, lure | NULL means no attachment; positive IDs identify one. |
| Gender / size | NULL means unspecified or inapplicable. Genderless is 3; size 0 is not Medium. |
| Team / weather / alignment | Preserve observed Neutral/NONE/normal zero; missing information stays NULL. |
| IVs, counts, booleans | Zero and false are real values. |
Events and their forts can arrive in either order; outer-join the parent if a missing gym or stop shouldn't drop the event.
Quests (sub_v2_quests)¶
PK quest_id (expires is indexed). quest_rewards is JSONB; for item
or encounter filters join sub_v2_quest_rewards (type, amount,
item_id, pokemon_id, form_id, encounter_shiny). Names are on
pogo_gm.items and pogo_gm.quest_reward_types.
Reward columns follow their variant: an item, currency, or candy reward doesn't carry encounter-only form, costume, gender, or shiny attributes. A species on a candy or energy reward identifies the resource's Pokémon; it isn't a caught encounter.
Places¶
Point, WOF, and timezone columns are on sub_v2_spawns, sub_v2_gyms,
sub_v2_pokestops, and sub_v2_stations. Events FK to those.
Spawnpoints have no sub_* view; spawnpoint_id is on the spawn.
Invasions¶
PK id (text). character / grunt_type are the grunt; lineup is
JSONB; expiration is the end time.
sub_v2_invasions_lineup is one row per slot (invasion_id, slot,
pokemon_id, form_id, form_group_id), same invasions dataset.
Filter by species like sub_v2_quest_rewards.
Power spots and max battles¶
A power spot is a non-overlapping window on a station, PK
(station_id, start_time).
A max battle is one fight, PK bread_battle_seed.
sub_v2_max_battle_events is the current hour at a station
(is_battle_available, boss columns, battle_start / battle_end). An
available event can keep its schedule before the boss is identified. An
explicit unavailable event clears both boss and window; omitted
availability doesn't overwrite the current event.
Weather¶
PK s2_cell_id (signed bigint, S2 level 10). gameplay_condition is the
weather ID (pogo_gm.weather). Boosted spawns use weather_boosted /
weather_id on the spawn, not the cell.
ID types¶
| Kind | Type |
|---|---|
Snowflake (spawn_id, raid_id, quest_id) |
bigint |
Game enum (pokemon_id, form_id, move_id) |
smallint |
Fort ID (gym_id, pokestop_id, station_id) |
text collation C |
Encounter (encounter_id) |
Opaque bigint |
Encounter IDs¶
Opaque. Golbat decimal uint64 string; stored as signed bigint. ::bigint
overflows — use numeric or:
u64_to_int8('17408421884750151680') -- wire → stored
int8_to_u64(encounter_id) -- stored → wire
-- wire → stored
CASE WHEN n >= 9223372036854775808
THEN (n - 18446744073709551616)::bigint ELSE n::bigint END
-- stored → wire
CASE WHEN encounter_id < 0
THEN encounter_id::numeric + 18446744073709551616
ELSE encounter_id::numeric END
Snowflakes¶
spawn_id, raid_id, and quest_id are Discord-epoch snowflakes: milliseconds
since 2015-01-01 UTC in the high bits (ms << 22). The low 22 bits are node and
sequence; time conversion shifts them off.
Helpers in public (EXECUTE is PUBLIC):
| Function | Returns |
|---|---|
snowflake_to_timestamp(id bigint) |
timestamptz of the id's millisecond |
timestamp_to_snowflake(ts timestamptz) |
Lower bound id for that instant (low 22 bits zero) |
snowflake_to_interval(delta bigint) |
Interval for an id delta |
interval_to_snowflake(dt interval) |
Id delta for a duration |
snowflake_now() |
Lower bound for "now" (clock_timestamp()) |
timestamp_to_snowflake / snowflake_now are range bounds. A lower bound
on spawn_id prunes partitions; snowflake_to_timestamp(spawn_id) does not.
SELECT spawn_id, snowflake_to_timestamp(spawn_id) AS minted
FROM pogo_live.sub_v2_spawns
WHERE spawn_id >= timestamp_to_snowflake(now() - interval '15 minutes')
AND expires > now();