Task S2T10: Create gk_users Counter Trigger + Attach¶
Master-Spec Alignment¶
The normative contract for this sprint is 00-SPRINT-INDEX.md plus the canonical Sprint 2 task set in docs/database-refactor/sprint-2/. ../00-SPEC-DRAFT-v1.obsolete.md is legacy context only and is not authoritative.
- Canonical previous-move function name:
geokrety.fn_set_previous_move_id_and_distance(). - Canonical
stats.daily_activitycolumn name:gk_created, notnew_geokrety_count. - Canonical
stats.daily_entity_countscolumn name:cnt, notcumulative_count. - Canonical
stats.daily_active_userscontract is presence-only:(activity_date, user_id)with no per-usermove_countcolumn. - The previous-move function must probe
geokrety.gk_geokrety.last_positionfirst, then fall back to ordered lookup ingeokrety.gk_moves. - Any lower sections that still use legacy names such as
fn_gk_moves_set_previous_move,new_geokrety_count,cumulative_count, ordistance_kmare obsolete draft text and are superseded by this alignment block.
Source¶
- Generated from sprint document step
2.10in02-sprint-2-counters-daily-activity.md.
Step 2.10: Create gk_users Counter Trigger + Attach¶
What this step does: Creates geokrety.fn_gk_users_counter() and attaches it as tr_gk_users_activity AFTER INSERT OR DELETE on geokrety.gk_users. This trigger maintains stats.entity_counters_shard for gk_users (total) and increments stats.daily_activity.users_registered for the registration calendar day. Shard = id % 16.
Migration file name: 20260310200900_create_gk_users_counter_trigger.php
Full SQL DDL¶
CREATE OR REPLACE FUNCTION geokrety.fn_gk_users_counter()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
v_shard INT;
v_delta INT;
v_date DATE;
BEGIN
IF TG_OP = 'INSERT' THEN
v_shard := NEW.id % 16;
v_delta := 1;
v_date := NEW.joined_on_datetime::date;
ELSE -- DELETE
v_shard := OLD.id % 16;
v_delta := -1;
v_date := OLD.joined_on_datetime::date;
END IF;
-- Increment/decrement total users shard
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_users', v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
-- Update daily activity user registration count
INSERT INTO stats.daily_activity (activity_date, users_registered)
VALUES (v_date, v_delta)
ON CONFLICT (activity_date) DO UPDATE SET
users_registered = stats.daily_activity.users_registered + v_delta;
RETURN NULL;
END;
$$;
CREATE TRIGGER tr_gk_users_activity
AFTER INSERT OR DELETE ON geokrety.gk_users
FOR EACH ROW EXECUTE FUNCTION geokrety.fn_gk_users_counter();
Full Phinx Migration PHP Code¶
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateGkUsersCounterTrigger extends AbstractMigration
{
public function up(): void
{
$this->execute(<<<'SQL'
CREATE OR REPLACE FUNCTION geokrety.fn_gk_users_counter()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
v_shard INT;
v_delta INT;
v_date DATE;
BEGIN
IF TG_OP = 'INSERT' THEN
v_shard := NEW.id % 16;
v_delta := 1;
v_date := NEW.joined_on_datetime::date;
ELSE -- DELETE
v_shard := OLD.id % 16;
v_delta := -1;
v_date := OLD.joined_on_datetime::date;
END IF;
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_users', v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
INSERT INTO stats.daily_activity (activity_date, users_registered)
VALUES (v_date, v_delta)
ON CONFLICT (activity_date) DO UPDATE SET
users_registered = stats.daily_activity.users_registered + v_delta;
RETURN NULL;
END;
$$;
CREATE TRIGGER tr_gk_users_activity
AFTER INSERT OR DELETE ON geokrety.gk_users
FOR EACH ROW EXECUTE FUNCTION geokrety.fn_gk_users_counter();
SQL
);
}
public function down(): void
{
$this->execute('DROP TRIGGER IF EXISTS tr_gk_users_activity ON geokrety.gk_users;');
$this->execute('DROP FUNCTION IF EXISTS geokrety.fn_gk_users_counter() CASCADE;');
}
}
SQL Usage Examples¶
-- Read total users count
SELECT SUM(cnt) AS total_users
FROM stats.entity_counters_shard WHERE entity = 'gk_users';
-- New user registrations per day in the last 30 days
SELECT activity_date, users_registered
FROM stats.daily_activity
WHERE users_registered > 0
AND activity_date >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY activity_date DESC;
-- Peak registration day ever
SELECT activity_date, users_registered
FROM stats.daily_activity
ORDER BY users_registered DESC
LIMIT 5;
Graph/Visualization Specification¶
Unlocked visualizations:
- Chart type: Line chart — User registrations per day (new user growth rate)
-
Data source:
SELECT activity_date, users_registered FROM stats.daily_activity WHERE users_registered > 0 ORDER BY activity_date -
KPI card: Total registered users
- Data source:
SELECT SUM(cnt) FROM stats.entity_counters_shard WHERE entity = 'gk_users'
ASCII Sample (User Registration Timeline):
2025-06 |█████████████████████ | 1,243 registrations
2025-05 |██████████████████████ | 1,312 registrations
2025-04 |███████████████████ | 988 registrations
2025-03 |████████████████ | 810 registrations
2025-02 |████████████ | 624 registrations
TimescaleDB Assessment¶
NOT applicable. This step creates a trigger function, not a table.
pgTAP Unit Tests¶
| Test ID | Test Name | Assertion |
|---|---|---|
| T-2.10.001 | Function fn_gk_users_counter exists | has_function('geokrety', 'fn_gk_users_counter', ARRAY[]::text[]) |
| T-2.10.002 | Function returns trigger | function_returns('geokrety', 'fn_gk_users_counter', ARRAY[]::text[], 'trigger') |
| T-2.10.003 | Trigger tr_gk_users_activity exists | has_trigger('geokrety', 'gk_users', 'tr_gk_users_activity') |
| T-2.10.004 | INSERT increments gk_users counter | Insert new user, verify SUM(cnt) WHERE entity='gk_users' increases by 1 |
| T-2.10.005 | INSERT updates daily_activity.users_registered | Insert user, verify daily_activity.users_registered increases by 1 |
| T-2.10.006 | DELETE decrements gk_users counter | Insert then delete user, verify counter returns to prior value |
| T-2.10.007 | Shard selection is id % 16 | Insert user with known id, verify correct shard row updated |
Implementation Checklist¶
- 1. Verify
stats.entity_counters_shardandstats.daily_activitytables exist (Steps 2.1, 2.2) - 2. Create migration file
20260310200900_create_gk_users_counter_trigger.php - 3. Run
phinx migrate - 4. Verify function
geokrety.fn_gk_users_counterexists - 5. Verify trigger
tr_gk_users_activityexists ongk_users - 6. Test INSERT → counter and daily_activity.users_registered incremented
- 7. Test DELETE → counters decremented
- 8. Run pgTAP tests T-2.10.001 through T-2.10.007