Task S5T07: Milestone Detection Trigger¶
Sprint Context¶
- Sprint index: Sprint 5 Task Index
- Tags: database, database-refactor, sprint-5, task-index
Source¶
- DBA source:
task-S5T07.dba.md - Specification source:
task-S5T07.specification.md
Resolved Decision¶
- S5T07 canonically owns the live milestone trigger only.
- Hourly activity and country-pair flows are batch/manual concerns owned by S5T09.
- Live first-finder detection is owned by S5T08.
- The canonical migration name remains
20260310500600_create_milestone_trigger.php.
Purpose & Scope¶
Creates the live milestone detection trigger function and attaches it to geokrety.gk_moves so qualifying inserts append milestone facts into stats.gk_milestone_events.
Scope:
- km milestones derived from qualifying kilometer-counting moves
- distinct-user milestones derived from canonical
stats.gk_related_users - one-time insert semantics into
stats.gk_milestone_events
Out of scope:
- first-finder detection
- hourly activity rollups
- country-pair flow rollups
Requirements¶
| ID | Description | MoSCoW |
|---|---|---|
| REQ-620 | Function geokrety.fn_gk_moves_milestones() exists |
MUST |
| REQ-621 | Trigger tr_gk_moves_after_milestones fires AFTER INSERT on geokrety.gk_moves |
MUST |
| REQ-622 | Kilometer-counting moves evaluate canonical km milestones at 100, 1000, and 10000 | MUST |
| REQ-623 | Distinct-user milestones evaluate canonical thresholds using stats.gk_related_users |
MUST |
| REQ-624 | Milestone inserts use one-time-event semantics via ON CONFLICT DO NOTHING |
MUST |
| REQ-625 | Trigger logic is limited to live milestone detection and append-only event creation | MUST |
| REQ-626 | phinx rollback drops trigger then function cleanly |
MUST |
Acceptance Criteria¶
| # | Criterion | How to Verify |
|---|---|---|
| 1 | Function created in geokrety schema |
\df geokrety.fn_gk_moves_milestones |
| 2 | Trigger attached to geokrety.gk_moves |
\d geokrety.gk_moves trigger list |
| 3 | Crossing 100 km inserts km_100 once |
Insert qualifying data twice; row count stays 1 |
| 4 | Crossing user threshold inserts user milestone once | Verify one row for the touched GK/event type |
| 5 | Non-qualifying inserts do not create milestone rows | Verify no new event |
| 6 | Rollback removes trigger and function | phinx rollback |
Migration File¶
20260310500600_create_milestone_trigger.php
Full SQL DDL¶
CREATE OR REPLACE FUNCTION geokrety.fn_gk_moves_milestones()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
-- On live INSERT only:
-- 1. Evaluate kilometer milestones from canonical km-counting move history.
-- 2. Evaluate user-reach milestones from canonical `stats.gk_related_users`.
-- 3. Append milestone rows into `stats.gk_milestone_events` with
-- `ON CONFLICT DO NOTHING` semantics.
RETURN NEW;
END;
$$;
CREATE TRIGGER tr_gk_moves_after_milestones
AFTER INSERT ON geokrety.gk_moves
FOR EACH ROW
EXECUTE FUNCTION geokrety.fn_gk_moves_milestones();
Full Phinx Migration PHP Code¶
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateMilestoneTrigger extends AbstractMigration
{
public function up(): void
{
$this->execute(<<<'SQL'
CREATE OR REPLACE FUNCTION geokrety.fn_gk_moves_milestones()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
RETURN NEW;
END;
$$;
CREATE TRIGGER tr_gk_moves_after_milestones
AFTER INSERT ON geokrety.gk_moves
FOR EACH ROW EXECUTE FUNCTION geokrety.fn_gk_moves_milestones();
SQL
);
}
public function down(): void
{
$this->execute(<<<'SQL'
DROP TRIGGER IF EXISTS tr_gk_moves_after_milestones ON geokrety.gk_moves;
DROP FUNCTION IF EXISTS geokrety.fn_gk_moves_milestones();
SQL
);
}
}
The placeholder function body above must be replaced by the canonical milestone checks required by REQ-622 through REQ-625.
Canonical Notes¶
- Kilometer milestones are derived only from the canonical km-counting move types.
- User-count milestones read the already-maintained
stats.gk_related_userstable instead of rebuilding relationships inside this trigger. - Any merged text assigning first-finder, hourly, or country-pair ownership to this task is obsolete.
pgTAP Unit Tests¶
| Test ID | Assertion | Pass Condition |
|---|---|---|
| T-5.7.001 | Function exists | has_function() |
| T-5.7.002 | Trigger exists | has_trigger() |
| T-5.7.003 | km_100 milestone inserted when threshold crossed |
exact match |
| T-5.7.004 | Re-check does not duplicate milestone rows | exact match |
| T-5.7.005 | User-threshold milestone inserted once | exact match |
| T-5.7.006 | Rollback removes trigger and function | pass |
Implementation Checklist¶
- 1. Create
20260310500600_create_milestone_trigger.php - 2.
phinx migrate— no errors - 3. Confirm
tr_gk_moves_after_milestonesin\d geokrety.gk_moves - 4. Verify km milestones at 100, 1000, and 10000
- 5. Verify user-count milestone insertion using seeded
stats.gk_related_users - 6. Run pgTAP T-5.7.001 through T-5.7.006
- 7.
phinx rollback— trigger and function dropped cleanly
Canonical Alignment¶
- S5T07 is the live milestone trigger task named in the Sprint 5 index.
- First-finder trigger logic belongs to S5T08.
- Batch/manual analytics rollups belong to S5T09.
Agent Loop Log¶
- 2026-03-10T19:55:00Z —
dba: removed hourly, country-pair, and first-finder trigger scope from S5T07 so the task matches the Sprint 5 migration plan. - 2026-03-10T19:55:00Z —
critical-thinking: separated live milestone detection from other analytics concerns to avoid overlapping trigger ownership. - 2026-03-10T19:55:00Z —
specification: canonized the migration name and narrowed the acceptance surface to milestone events only.
Resolution¶
Q-033 is resolved by canonizing S5T07 as the milestone detection trigger task only.