Task S2T04: Create stats.daily_entity_counts Table¶
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.4in02-sprint-2-counters-daily-activity.md.
Step 2.4: Create stats.daily_entity_counts Table¶
What this step does: Creates the stats.daily_entity_counts table that stores daily entity snapshot totals by calendar day. This is the time-series companion to entity_counters_shard: while the shard table gives the current live total, daily_entity_counts records the daily seeded value used by charts and reconciliations. A nightly/manual job (Sprint 6) inserts one row per (count_date, entity) by reading the shard totals.
Migration file name: 20260310200300_create_daily_entity_counts.php
Full SQL DDL¶
CREATE TABLE stats.daily_entity_counts (
count_date DATE NOT NULL,
entity VARCHAR(32) NOT NULL,
cnt BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (count_date, entity)
);
COMMENT ON TABLE stats.daily_entity_counts IS 'Daily cumulative entity counts for trend charts; populated by nightly snapshot job';
COMMENT ON COLUMN stats.daily_entity_counts.entity IS 'Entity name matching entity_counters_shard.entity';
COMMENT ON COLUMN stats.daily_entity_counts.cnt IS 'Entity count snapshot value for count_date';
Full Phinx Migration PHP Code¶
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateDailyEntityCounts extends AbstractMigration
{
public function up(): void
{
$this->execute(<<<'SQL'
CREATE TABLE stats.daily_entity_counts (
count_date DATE NOT NULL,
entity VARCHAR(32) NOT NULL,
cnt BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (count_date, entity)
);
COMMENT ON TABLE stats.daily_entity_counts IS 'Daily cumulative entity counts for trend charts; populated by nightly snapshot job';
COMMENT ON COLUMN stats.daily_entity_counts.entity IS 'Entity name matching entity_counters_shard.entity';
COMMENT ON COLUMN stats.daily_entity_counts.cnt IS 'Entity count snapshot value for count_date';
SQL
);
}
public function down(): void
{
$this->execute('DROP TABLE IF EXISTS stats.daily_entity_counts;');
}
}
SQL Usage Examples¶
-- GeoKrety daily snapshot counts for the last year
SELECT count_date, cnt
FROM stats.daily_entity_counts
WHERE entity = 'gk_geokrety'
AND count_date >= CURRENT_DATE - INTERVAL '1 year'
ORDER BY count_date;
-- Platform growth chart: all major entities over time
SELECT count_date,
MAX(CASE WHEN entity = 'gk_moves' THEN cnt END) AS total_moves,
MAX(CASE WHEN entity = 'gk_geokrety' THEN cnt END) AS total_geokrety,
MAX(CASE WHEN entity = 'gk_users' THEN cnt END) AS total_users
FROM stats.daily_entity_counts
WHERE entity IN ('gk_moves', 'gk_geokrety', 'gk_users')
GROUP BY count_date
ORDER BY count_date;
-- Growth rate: day-over-day new moves (delta from snapshot counts)
SELECT count_date,
cnt,
cnt - LAG(cnt, 1, 0) OVER (ORDER BY count_date) AS daily_new
FROM stats.daily_entity_counts
WHERE entity = 'gk_moves'
ORDER BY count_date;
-- Upsert today's snapshot (called by nightly job)
INSERT INTO stats.daily_entity_counts (count_date, entity, cnt)
SELECT CURRENT_DATE, entity, SUM(cnt)
FROM stats.entity_counters_shard
GROUP BY entity
ON CONFLICT (count_date, entity) DO UPDATE SET
cnt = EXCLUDED.cnt;
Graph/Visualization Specification¶
Unlocked visualizations:
- Chart type: Multi-line area chart — Platform Growth over time
- X-axis:
count_date - Y-axis (left):
cntforgk_moves - Y-axis (right):
cntforgk_geokretyandgk_users -
Data sources: Three series filtered by entity name
-
Chart type: Stacked area chart — GK type composition evolution over time
- X-axis:
count_date - Series:
gk_geokrety_type_0throughgk_geokrety_type_10
ASCII Sample (Platform Growth):
Moves (M) Users (K) GeoKrety (K)
7.0 | ● |
6.0 | ● ● | 150 · ·
5.0 | ● ● | 120 · ·
4.0 | ● ● | 90
3.0 |● ● ● | 60
2.0 | ● ● ●| 30
+---------------------------+
2015 2017 2019 2021 2023 2025
TimescaleDB Assessment¶
RECOMMENDED. Rationale:
daily_entity_countsis a canonical time-series table: rows are inserted once per day per entity, never updated, and accessed primarily via time-range queries (e.g., "last year of data").- With 25 entities × 365 days/year × 15 years = ~136,875 rows today, growing at ~9,125 rows/year.
- While small now, TimescaleDB hypertable on
count_datewould enable: - Automatic chunk-based time pruning for date-range scans
- Column-based compression (up to 90% storage savings for such repetitive data)
- Continuous aggregates if sub-daily granularity is added later
- Recommendation: Convert to TimescaleDB hypertable if TimescaleDB is installed. Otherwise, standard PostgreSQL PK index is sufficient at current scale.
pgTAP Unit Tests¶
| Test ID | Test Name | Assertion |
|---|---|---|
| T-2.4.001 | daily_entity_counts table exists | has_table('stats', 'daily_entity_counts') |
| T-2.4.002 | PK is (count_date, entity) | col_is_pk('stats', 'daily_entity_counts', ARRAY['count_date', 'entity']) |
| T-2.4.003 | count_date type is date | col_type_is('stats', 'daily_entity_counts', 'count_date', 'date') |
| T-2.4.004 | entity type is varchar(32) | col_type_is('stats', 'daily_entity_counts', 'entity', 'character varying(32)') |
| T-2.4.005 | cnt default is 0 | col_default_is('stats', 'daily_entity_counts', 'cnt', '0') |
| T-2.4.006 | cnt type is bigint | col_type_is('stats', 'daily_entity_counts', 'cnt', 'bigint') |
| T-2.4.007 | Insert succeeds | Insert ('2025-06-15', 'gk_moves', 6000000) and verify |
| T-2.4.008 | Duplicate PK raises error | Insert same (count_date, entity) twice — throws_ok |
| T-2.4.009 | ON CONFLICT upsert works | Insert then re-insert with different value using ON CONFLICT, verify updated |
Implementation Checklist¶
- 1. Create migration file
20260310200300_create_daily_entity_counts.php - 2. Run
phinx migrate - 3. Verify table exists with 3 columns and correct types
- 4. Verify composite PK on
(count_date, entity) - 5. Test insert, read-back, and upsert behavior
- 6. Run pgTAP tests T-2.4.001 through T-2.4.009