Task S3T02: Create stats.gk_countries_visited Table¶
Master-Spec Alignment¶
The normative contract for this sprint is 00-SPEC-DRAFT-v1.obsolete.md, Sections 5.3, 5.4, 8.4, 9.2, and 11.
stats.country_daily_stats.unique_usersandunique_gksare exact online-maintained values, not approximate placeholders.INSERT,UPDATE, andDELETEhandling forstats.gk_countries_visited,stats.user_countries, andstats.gk_country_historymust maintain exact state. When earliest/latest rows are invalidated, affected rows must be recomputed from remaining qualifying moves.- Snapshot functions seed and verify canonical state; they do not compensate for knowingly inexact live maintenance.
- Any lower text that still describes
unique_usersorunique_gksas approximate is obsolete and superseded by this alignment block.
Source¶
- Generated from sprint document step
3.2in03-sprint-3-country-geography.md.
Step 3.2: Create stats.gk_countries_visited Table¶
What this step does: Creates the stats.gk_countries_visited table that tracks which countries each GeoKret has visited, including first-visit metadata and move counts. This table enables "countries visited" badges, GK travel maps, and country-crossing detection for the gamification multiplier system (+0.05 per new country).
Migration file name: 20260310300100_create_gk_countries_visited.php
Full SQL DDL¶
CREATE TABLE stats.gk_countries_visited (
geokrety_id INT NOT NULL,
country_code CHAR(2) NOT NULL,
first_visited_at TIMESTAMPTZ NOT NULL,
first_move_id BIGINT NOT NULL,
move_count INT NOT NULL DEFAULT 1,
PRIMARY KEY (geokrety_id, country_code)
);
COMMENT ON TABLE stats.gk_countries_visited IS 'Tracks which countries each GK has visited, with first-visit metadata and move counts';
COMMENT ON COLUMN stats.gk_countries_visited.first_move_id IS 'ID of the first move that placed this GK in this country';
COMMENT ON COLUMN stats.gk_countries_visited.move_count IS 'Total number of moves by this GK in this country';
Full Phinx Migration PHP Code¶
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateGkCountriesVisited extends AbstractMigration
{
public function up(): void
{
$this->execute(<<<'SQL'
CREATE TABLE stats.gk_countries_visited (
geokrety_id INT NOT NULL,
country_code CHAR(2) NOT NULL,
first_visited_at TIMESTAMPTZ NOT NULL,
first_move_id BIGINT NOT NULL,
move_count INT NOT NULL DEFAULT 1,
PRIMARY KEY (geokrety_id, country_code)
);
COMMENT ON TABLE stats.gk_countries_visited IS 'Tracks which countries each GK has visited, with first-visit metadata and move counts';
COMMENT ON COLUMN stats.gk_countries_visited.first_move_id IS 'ID of the first move that placed this GK in this country';
COMMENT ON COLUMN stats.gk_countries_visited.move_count IS 'Total number of moves by this GK in this country';
SQL
);
}
public function down(): void
{
$this->execute('DROP TABLE IF EXISTS stats.gk_countries_visited;');
}
}
SQL Usage Examples¶
-- How many countries has GK #1 visited?
SELECT COUNT(*) AS countries_visited
FROM stats.gk_countries_visited
WHERE geokrety_id = 1;
-- Travel history for GK #1 in country visit order
SELECT country_code, first_visited_at, first_move_id, move_count
FROM stats.gk_countries_visited
WHERE geokrety_id = 1
ORDER BY first_visited_at ASC;
-- Top 10 most-traveled GKs by country count
SELECT geokrety_id, COUNT(*) AS countries
FROM stats.gk_countries_visited
GROUP BY geokrety_id
ORDER BY countries DESC
LIMIT 10;
-- GKs that have visited both Poland and Germany
SELECT v1.geokrety_id
FROM stats.gk_countries_visited v1
JOIN stats.gk_countries_visited v2 ON v1.geokrety_id = v2.geokrety_id
WHERE v1.country_code = 'PL' AND v2.country_code = 'DE';
Graph/Visualization Specification¶
Unlocked visualizations:
- Chart type: Bar chart — top GKs by country count
- X-axis: GK tracking code (GK + hex ID)
-
Y-axis:
COUNT(*)of countries visited -
Chart type: Route map — GK travel path across countries in order of
first_visited_at - Data source:
SELECT country_code, first_visited_at FROM stats.gk_countries_visited WHERE geokrety_id = ? ORDER BY first_visited_at
ASCII Sample (GK Travel Path):
GK0001: PL → DE → CZ → AT → CH → FR → ES (7 countries)
GK0042: PL → SK → HU → RO → BG → GR (6 countries)
Route-map visualizations should present the ordered unique-country visitation sequence derived from stats.gk_countries_visited; repeated later returns do not create duplicate country rows in this table.
TimescaleDB Assessment¶
NOT recommended. This is a lookup/dimension table keyed by (geokrety_id, country_code). There is no time column suitable for hypertable partitioning. Row count grows proportionally to unique_GKs × unique_countries_per_GK (~200K–500K rows), well within standard PostgreSQL capabilities.
pgTAP Unit Tests¶
| Test ID | Test Name | Assertion |
|---|---|---|
| T-3.2.001 | gk_countries_visited table exists | has_table('stats', 'gk_countries_visited') |
| T-3.2.002 | PK is (geokrety_id, country_code) | col_is_pk('stats', 'gk_countries_visited', ARRAY['geokrety_id', 'country_code']) |
| T-3.2.003 | geokrety_id type is integer | col_type_is('stats', 'gk_countries_visited', 'geokrety_id', 'integer') |
| T-3.2.004 | country_code type is char(2) | col_type_is('stats', 'gk_countries_visited', 'country_code', 'character(2)') |
| T-3.2.005 | first_visited_at type is timestamptz | col_type_is('stats', 'gk_countries_visited', 'first_visited_at', 'timestamp with time zone') |
| T-3.2.006 | first_move_id is NOT NULL | col_not_null('stats', 'gk_countries_visited', 'first_move_id') |
| T-3.2.007 | move_count default is 1 | col_default_is('stats', 'gk_countries_visited', 'move_count', '1') |
| T-3.2.008 | Insert and read-back succeeds | Insert (1, 'PL', now(), 100, 1) and verify |
| T-3.2.009 | Duplicate PK raises error | Insert same (geokrety_id, country_code) twice — throws_ok |
Implementation Checklist¶
- 1. Create migration file
20260310300100_create_gk_countries_visited.php - 2. Run
phinx migrate - 3. Verify table exists with 5 columns and correct types
- 4. Verify composite PK on
(geokrety_id, country_code) - 5. Test insert and duplicate rejection
- 6. Run pgTAP tests T-3.2.001 through T-3.2.009