dbPath); if (!is_dir($dir)) { mkdir($dir, 0755, recursive: true); } $this->pdo = new PDO('sqlite:' . $this->dbPath); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->prepareSchema(); } private function prepareSchema(): void { $this->pdo->exec(' CREATE TABLE IF NOT EXISTS contributions ( provider TEXT NOT NULL, date INTEGER NOT NULL, count INTEGER NOT NULL CHECK (count >= 0), PRIMARY KEY (provider, date) ) WITHOUT ROWID, STRICT '); } /** * Inserts a contribution count, overwriting any existing count for the same provider/date. * * @param string $provider provider identifier, e.g. "github" * @param int $unixtime day of the contribution, as a unix timestamp * @param int $count contribution count for that day */ public function add(string $provider, int $unixtime, int $count): void { $stmt = $this->pdo->prepare(' INSERT INTO contributions (provider, date, count) VALUES (?, ?, ?) ON CONFLICT(provider, date) DO UPDATE SET count = excluded.count '); $stmt->execute([$provider, $unixtime, $count]); } /** * Deletes the contribution row for the given provider/date, if any. * * @param string $provider provider identifier, e.g. "github" * @param int $unixtime day to delete, as a unix timestamp */ public function remove(string $provider, int $unixtime): void { $stmt = $this->pdo->prepare(' DELETE FROM contributions WHERE provider = ? AND date = ? '); $stmt->execute([$provider, $unixtime]); } /** * Upserts a batch of date => count pairs for a provider via repeated add() calls. * * @param string $provider provider identifier, e.g. "github" * @param array $dateCounts unix timestamp => count */ public function merge(string $provider, array $dateCounts): void { foreach ($dateCounts as $unixtime => $count) { $this->add($provider, $unixtime, $count); } } /** * Returns the most recent stored unix timestamp for a provider, or null if it has no rows. * * @param string $provider provider identifier, e.g. "github" * @return ?int unix timestamp of the latest stored day, or null if none stored yet */ public function latestDate(string $provider): ?int { $stmt = $this->pdo->prepare('SELECT MAX(date) FROM contributions WHERE provider = ?'); $stmt->execute([$provider]); $result = $stmt->fetchColumn(); return $result !== null ? (int) $result : null; } /** * Returns all stored contributions for a provider, optionally limited to the last $sinceDays days. * * @param string $provider provider identifier, e.g. "github" * @param ?int $sinceDays if set, only rows from the last N days are returned * @return ContributionCollection */ public function all(string $provider, ?int $sinceDays = null): ContributionCollection { $sql = 'SELECT provider, date, count FROM contributions WHERE provider = ?'; $params = [$provider]; if ($sinceDays !== null) { $sql .= ' AND date >= ?'; $params[] = time() - $sinceDays * self::DAY_IN_SECONDS; } $stmt = $this->pdo->prepare($sql); $stmt->execute($params); /** @var array $rows */ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $contributions = array_map( static fn(array $row): Contribution => new Contribution($row['provider'], (int) $row['date'], (int) $row['count']), $rows, ); return new ContributionCollection(...$contributions); } /** * Deletes rows older than the configured retention window; a no-op if retention is unset or 0. */ public function prune(): void { if ($this->retentionDays === null || $this->retentionDays === 0) { return; } $cutoff = time() - $this->retentionDays * self::DAY_IN_SECONDS; $stmt = $this->pdo->prepare('DELETE FROM contributions WHERE date < ?'); $stmt->execute([$cutoff]); } }