Task S2T08: Create gk_geokrety 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.8in02-sprint-2-counters-daily-activity.md.
Step 2.8: Create gk_geokrety Counter Trigger + Attach¶
What this step does: Creates geokrety.fn_gk_geokrety_counter() and attaches it as tr_gk_geokrety_counters AFTER INSERT OR DELETE on geokrety.gk_geokrety. This trigger maintains stats.entity_counters_shard for gk_geokrety (total) and gk_geokrety_type_0 through gk_geokrety_type_10 (per GK type), and increments stats.daily_activity.gk_created for the GeoKret creation calendar day. Shard = id % 16.
Migration file name: 20260310200700_create_gk_geokrety_counter_trigger.php
Full SQL DDL¶
CREATE OR REPLACE FUNCTION geokrety.fn_gk_geokrety_counter()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
v_shard INT;
v_gk_type INT;
v_delta INT;
v_date DATE;
BEGIN
IF TG_OP = 'INSERT' THEN
v_shard := NEW.id % 16;
v_gk_type := NEW.type;
v_delta := 1;
v_date := NEW.created_on_datetime::date;
ELSE -- DELETE
v_shard := OLD.id % 16;
v_gk_type := OLD.type;
v_delta := -1;
v_date := OLD.created_on_datetime::date;
END IF;
-- Increment/decrement total geokrety shard
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_geokrety', v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
-- Increment/decrement per-type shard (types 0..10 only)
IF v_gk_type BETWEEN 0 AND 10 THEN
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_geokrety_type_' || v_gk_type::text, v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
END IF;
-- Update daily activity GeoKrety creation count
INSERT INTO stats.daily_activity (activity_date, gk_created)
VALUES (v_date, v_delta)
ON CONFLICT (activity_date) DO UPDATE SET
gk_created = stats.daily_activity.gk_created + v_delta;
RETURN NULL;
END;
$$;
CREATE TRIGGER tr_gk_geokrety_counters
AFTER INSERT OR DELETE ON geokrety.gk_geokrety
FOR EACH ROW EXECUTE FUNCTION geokrety.fn_gk_geokrety_counter();
Full Phinx Migration PHP Code¶
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateGkGeokretyCounterTrigger extends AbstractMigration
{
public function up(): void
{
$this->execute(<<<'SQL'
CREATE OR REPLACE FUNCTION geokrety.fn_gk_geokrety_counter()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
v_shard INT;
v_gk_type INT;
v_delta INT;
v_date DATE;
BEGIN
IF TG_OP = 'INSERT' THEN
v_shard := NEW.id % 16;
v_gk_type := NEW.type;
v_delta := 1;
v_date := NEW.created_on_datetime::date;
ELSE -- DELETE
v_shard := OLD.id % 16;
v_gk_type := OLD.type;
v_delta := -1;
v_date := OLD.created_on_datetime::date;
END IF;
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_geokrety', v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
IF v_gk_type BETWEEN 0 AND 10 THEN
INSERT INTO stats.entity_counters_shard (entity, shard, cnt)
VALUES ('gk_geokrety_type_' || v_gk_type::text, v_shard, v_delta)
ON CONFLICT (entity, shard)
DO UPDATE SET cnt = stats.entity_counters_shard.cnt + v_delta;
END IF;
INSERT INTO stats.daily_activity (activity_date, gk_created)
VALUES (v_date, v_delta)
ON CONFLICT (activity_date) DO UPDATE SET
gk_created = stats.daily_activity.gk_created + v_delta;
RETURN NULL;
END;
$$;
CREATE TRIGGER tr_gk_geokrety_counters
AFTER INSERT OR DELETE ON geokrety.gk_geokrety
FOR EACH ROW EXECUTE FUNCTION geokrety.fn_gk_geokrety_counter();
SQL
);
}
public function down(): void
{
$this->execute('DROP TRIGGER IF EXISTS tr_gk_geokrety_counters ON geokrety.gk_geokrety;');
$this->execute('DROP FUNCTION IF EXISTS geokrety.fn_gk_geokrety_counter() CASCADE;');
}
}
SQL Usage Examples¶
-- Read total GeoKrety count
SELECT SUM(cnt) AS total_geokrety
FROM stats.entity_counters_shard WHERE entity = 'gk_geokrety';
-- Read GeoKrety type distribution
SELECT entity, SUM(cnt) AS total
FROM stats.entity_counters_shard
WHERE entity LIKE 'gk_geokrety_type_%'
GROUP BY entity
ORDER BY entity;
-- Check how many new GeoKrety were created today
SELECT gk_created FROM stats.daily_activity
WHERE activity_date = CURRENT_DATE;
Graph/Visualization Specification¶
Unlocked visualizations:
- Chart type: Donut chart — GeoKrety type distribution
-
Data source:
SELECT entity, SUM(cnt) FROM stats.entity_counters_shard WHERE entity LIKE 'gk_geokrety_type_%' GROUP BY entity -
Chart type: Line chart — New GeoKrety created per day
- Data source:
SELECT activity_date, gk_created FROM stats.daily_activity ORDER BY activity_date
ASCII Sample (GK Type Distribution):
Traditional(0) |████████████████████████████| 68%
Book/DVD(1) |████ | 9%
Coin(3) |███ | 7%
Human(2) |██ | 5%
KretyPost(4) |██ | 4%
Other |███ | 7%
TimescaleDB Assessment¶
NOT applicable. This step creates a trigger function, not a table.
pgTAP Unit Tests¶
| Test ID | Test Name | Assertion |
|---|---|---|
| T-2.8.001 | Function fn_gk_geokrety_counter exists | has_function('geokrety', 'fn_gk_geokrety_counter', ARRAY[]::text[]) |
| T-2.8.002 | Function returns trigger | function_returns('geokrety', 'fn_gk_geokrety_counter', ARRAY[]::text[], 'trigger') |
| T-2.8.003 | Trigger tr_gk_geokrety_counters exists | has_trigger('geokrety', 'gk_geokrety', 'tr_gk_geokrety_counters') |
| T-2.8.004 | INSERT increments gk_geokrety | Insert new GK, verify SUM(cnt) WHERE entity='gk_geokrety' increases by 1 |
| T-2.8.005 | INSERT increments correct type shard | Insert GK of type 0, verify SUM(cnt) WHERE entity='gk_geokrety_type_0' increases |
| T-2.8.006 | INSERT updates daily_activity.gk_created | Insert GK, verify daily_activity.gk_created increases by 1 |
| T-2.8.007 | DELETE decrements gk_geokrety | Insert then delete GK, verify counter returns to prior value |
| T-2.8.008 | All 11 type entities (0..10) are updated correctly | Insert one GK of each type 0-10, verify all type counters incremented |
Implementation Checklist¶
- 1. Verify
stats.entity_counters_shardandstats.daily_activitytables exist (Steps 2.1, 2.2) - 2. Create migration file
20260310200700_create_gk_geokrety_counter_trigger.php - 3. Run
phinx migrate - 4. Verify function
geokrety.fn_gk_geokrety_counterexists - 5. Verify trigger
tr_gk_geokrety_countersexists ongk_geokrety - 6. Test INSERT → both shard counters and daily_activity.gk_created increment
- 7. Test DELETE → counters decrement
- 8. Run pgTAP tests T-2.8.001 through T-2.8.008