Skip to content

Commit 40398f5

Browse files
lohanidamodarclaude
andcommitted
feat: add max param to count() for bounded counts
Push the count cap down into the DB layer so callers that only need a capped total (e.g. rendering "5000+") can stop ClickHouse early instead of scanning the full filtered set. ClickHouse wraps the count in a LIMIT-bounded subquery; Database delegates to utopia-php/database's existing $max arg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d6ee1ac commit 40398f5

4 files changed

Lines changed: 48 additions & 15 deletions

File tree

src/Usage/Adapter.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,17 @@ abstract public function find(array $queries = [], ?string $type = null): array;
9797
/**
9898
* Count metrics using Query objects.
9999
*
100+
* When $max is non-null the count is bounded at the database level —
101+
* the adapter must stop counting once $max rows have been matched.
102+
* This keeps large counts cheap for endpoints that only need a capped
103+
* total. When $max is null the count is unbounded.
104+
*
100105
* @param array<\Utopia\Query\Query> $queries
101106
* @param string|null $type Metric type: 'event', 'gauge', or null (count both)
107+
* @param int|null $max Optional upper bound for the count (inclusive)
102108
* @return int
103109
*/
104-
abstract public function count(array $queries = [], ?string $type = null): int;
110+
abstract public function count(array $queries = [], ?string $type = null, ?int $max = null): int;
105111

106112
/**
107113
* Sum metric values using Query objects.

src/Usage/Adapter/ClickHouse.php

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,33 +1625,39 @@ private function parseAggregatedResults(string $result, string $type = 'event'):
16251625
/**
16261626
* Count metrics using Query objects.
16271627
*
1628+
* When $max is non-null the count is bounded at the database level via
1629+
* a `LIMIT {max}` inside a subquery — ClickHouse stops scanning once
1630+
* that many rows have been matched, keeping large counts cheap.
1631+
*
16281632
* @param array<Query> $queries
16291633
* @param string|null $type 'event', 'gauge', or null (both)
1634+
* @param int|null $max Optional upper bound (inclusive) for the count
16301635
* @return int
16311636
* @throws Exception
16321637
*/
1633-
public function count(array $queries = [], ?string $type = null): int
1638+
public function count(array $queries = [], ?string $type = null, ?int $max = null): int
16341639
{
16351640
$this->setOperationContext('count()');
16361641

16371642
if ($type !== null) {
1638-
return $this->countFromTable($queries, $type);
1643+
return $this->countFromTable($queries, $type, $max);
16391644
}
16401645

16411646
// Count from both tables
1642-
return $this->countFromTable($queries, Usage::TYPE_EVENT)
1643-
+ $this->countFromTable($queries, Usage::TYPE_GAUGE);
1647+
return $this->countFromTable($queries, Usage::TYPE_EVENT, $max)
1648+
+ $this->countFromTable($queries, Usage::TYPE_GAUGE, $max);
16441649
}
16451650

16461651
/**
16471652
* Count metrics from a specific table.
16481653
*
16491654
* @param array<Query> $queries
16501655
* @param string $type
1656+
* @param int|null $max Optional upper bound (inclusive) for the count
16511657
* @return int
16521658
* @throws Exception
16531659
*/
1654-
private function countFromTable(array $queries, string $type): int
1660+
private function countFromTable(array $queries, string $type, ?int $max = null): int
16551661
{
16561662
$tableName = $this->getTableForType($type);
16571663
$fromTable = $this->buildTableReference($tableName);
@@ -1665,10 +1671,20 @@ private function countFromTable(array $queries, string $type): int
16651671
$whereClause = $whereData['clause'];
16661672
$params = $whereData['params'];
16671673

1668-
$sql = "
1669-
SELECT COUNT(*) as total FROM {$fromTable}{$whereClause}
1670-
FORMAT JSON
1671-
";
1674+
if ($max !== null) {
1675+
$params['max'] = $max;
1676+
$sql = "
1677+
SELECT COUNT(*) as total FROM (
1678+
SELECT 1 FROM {$fromTable}{$whereClause} LIMIT {max:UInt64}
1679+
) sub
1680+
FORMAT JSON
1681+
";
1682+
} else {
1683+
$sql = "
1684+
SELECT COUNT(*) as total FROM {$fromTable}{$whereClause}
1685+
FORMAT JSON
1686+
";
1687+
}
16721688

16731689
$result = $this->query($sql, $params);
16741690
$json = json_decode($result, true);

src/Usage/Adapter/Database.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,18 +455,24 @@ public function find(array $queries = [], ?string $type = null): array
455455
/**
456456
* Count metrics using Query objects.
457457
*
458+
* When $max is non-null the count is bounded at the database level —
459+
* utopia-php/database accepts a `$max` argument that pushes the cap
460+
* down into the underlying SQL.
461+
*
458462
* @param array<Query> $queries
459463
* @param string|null $type
464+
* @param int|null $max Optional upper bound (inclusive) for the count
460465
* @return int
461466
*/
462-
public function count(array $queries = [], ?string $type = null): int
467+
public function count(array $queries = [], ?string $type = null, ?int $max = null): int
463468
{
464469
/** @var int $count */
465-
$count = $this->db->getAuthorization()->skip(function () use ($queries) {
470+
$count = $this->db->getAuthorization()->skip(function () use ($queries, $max) {
466471
$dbQueries = $this->convertQueriesToDatabase($queries);
467472
return $this->db->count(
468473
collection: $this->collection,
469-
queries: $dbQueries
474+
queries: $dbQueries,
475+
max: $max
470476
);
471477
});
472478

src/Usage/Usage.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,19 @@ public function find(array $queries = [], ?string $type = null): array
173173
/**
174174
* Count metrics using Query objects.
175175
*
176+
* When $max is non-null the count is bounded at the database level.
177+
* Callers that only need a capped total (e.g. to render "5000+") should
178+
* pass $max so the adapter can short-circuit the count for large tables.
179+
*
176180
* @param array<\Utopia\Query\Query> $queries
177181
* @param string|null $type Metric type: 'event', 'gauge', or null (count both)
182+
* @param int|null $max Optional upper bound for the count (inclusive)
178183
* @return int
179184
* @throws \Exception
180185
*/
181-
public function count(array $queries = [], ?string $type = null): int
186+
public function count(array $queries = [], ?string $type = null, ?int $max = null): int
182187
{
183-
return $this->adapter->count($queries, $type);
188+
return $this->adapter->count($queries, $type, $max);
184189
}
185190

186191
/**

0 commit comments

Comments
 (0)