Skip to content

Commit aac7b95

Browse files
committed
feat(usage-query): add groupBy method for dimensional aggregation
Library-private extension that lives on UsageQuery (not the base utopia-php/query Query class) so cloud can express "group by service over 1d", "group by path x status over 1h" requests without forcing an SDK regeneration across every Appwrite client. - TYPE_GROUP_BY constant + static factory groupBy(attribute). - isMethod() accepts the new method so Query::parse() round-trips it from the JSON wire form. - isGroupBy / extractGroupBy / removeGroupBy helpers. extractGroupBy returns an array (multiple groupBy queries may coexist when bucketing by several dimensions at once), unlike extractGroupByInterval which is single-instance. - Unit tests cover the factory, isMethod, helpers, parse round-trip and the parsed-Query (base class) extraction path.
1 parent ca09149 commit aac7b95

2 files changed

Lines changed: 159 additions & 2 deletions

File tree

src/Usage/UsageQuery.php

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
class UsageQuery extends Query
3131
{
3232
public const TYPE_GROUP_BY_INTERVAL = 'groupByInterval';
33+
public const TYPE_GROUP_BY = 'groupBy';
3334

3435
/**
3536
* Valid interval values and their ClickHouse INTERVAL equivalents.
@@ -45,11 +46,11 @@ class UsageQuery extends Query
4546
];
4647

4748
/**
48-
* Override isMethod to accept groupByInterval in addition to all base Query methods.
49+
* Override isMethod to accept groupByInterval and groupBy in addition to all base Query methods.
4950
*/
5051
public static function isMethod(string $value): bool
5152
{
52-
if ($value === self::TYPE_GROUP_BY_INTERVAL) {
53+
if ($value === self::TYPE_GROUP_BY_INTERVAL || $value === self::TYPE_GROUP_BY) {
5354
return true;
5455
}
5556

@@ -122,4 +123,60 @@ public static function removeGroupByInterval(array $queries): array
122123
return !self::isGroupByInterval($query);
123124
}));
124125
}
126+
127+
/**
128+
* Create a groupBy query for dimensional aggregation.
129+
*
130+
* Buckets results by the given attribute in addition to the time bucket
131+
* supplied via `groupByInterval`. Multiple `groupBy` queries may be
132+
* combined to bucket by several dimensions at once (e.g. service x status).
133+
*
134+
* @param string $attribute The dimension column to bucket on (service, path, status, ...).
135+
* @return self
136+
*/
137+
public static function groupBy(string $attribute): self
138+
{
139+
return new self(self::TYPE_GROUP_BY, $attribute, []);
140+
}
141+
142+
/**
143+
* Check if a query is a groupBy query.
144+
*
145+
* @param Query $query
146+
* @return bool
147+
*/
148+
public static function isGroupBy(Query $query): bool
149+
{
150+
return $query->getMethod() === self::TYPE_GROUP_BY;
151+
}
152+
153+
/**
154+
* Extract all groupBy queries from an array of queries.
155+
*
156+
* Multiple groupBy queries can coexist (group by service AND status), so
157+
* this returns every match rather than the single-instance form used by
158+
* groupByInterval.
159+
*
160+
* @param array<Query> $queries
161+
* @return array<Query>
162+
*/
163+
public static function extractGroupBy(array $queries): array
164+
{
165+
return array_values(array_filter($queries, function (Query $query) {
166+
return self::isGroupBy($query);
167+
}));
168+
}
169+
170+
/**
171+
* Remove all groupBy queries from an array of queries.
172+
*
173+
* @param array<Query> $queries
174+
* @return array<Query>
175+
*/
176+
public static function removeGroupBy(array $queries): array
177+
{
178+
return array_values(array_filter($queries, function (Query $query) {
179+
return !self::isGroupBy($query);
180+
}));
181+
}
125182
}

tests/Usage/UsageQueryTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,104 @@ public function testUsageQueryExtendsQuery(): void
131131
$query = UsageQuery::groupByInterval('time', '1h');
132132
$this->assertInstanceOf(Query::class, $query);
133133
}
134+
135+
public function testGroupByCreation(): void
136+
{
137+
$query = UsageQuery::groupBy('service');
138+
139+
$this->assertInstanceOf(UsageQuery::class, $query);
140+
$this->assertEquals(UsageQuery::TYPE_GROUP_BY, $query->getMethod());
141+
$this->assertEquals('service', $query->getAttribute());
142+
$this->assertEquals([], $query->getValues());
143+
}
144+
145+
public function testGroupByIsMethod(): void
146+
{
147+
$this->assertTrue(UsageQuery::isMethod(UsageQuery::TYPE_GROUP_BY));
148+
$this->assertTrue(UsageQuery::isMethod(UsageQuery::TYPE_GROUP_BY_INTERVAL));
149+
$this->assertTrue(UsageQuery::isMethod(Query::TYPE_EQUAL));
150+
$this->assertFalse(UsageQuery::isMethod('notARealMethod'));
151+
}
152+
153+
public function testIsGroupBy(): void
154+
{
155+
$groupBy = UsageQuery::groupBy('service');
156+
$groupByInterval = UsageQuery::groupByInterval('time', '1h');
157+
$regular = Query::equal('metric', ['bandwidth']);
158+
159+
$this->assertTrue(UsageQuery::isGroupBy($groupBy));
160+
$this->assertFalse(UsageQuery::isGroupBy($groupByInterval));
161+
$this->assertFalse(UsageQuery::isGroupBy($regular));
162+
}
163+
164+
public function testExtractGroupByReturnsAllMatches(): void
165+
{
166+
$byService = UsageQuery::groupBy('service');
167+
$byPath = UsageQuery::groupBy('path');
168+
$interval = UsageQuery::groupByInterval('time', '1h');
169+
$equal = Query::equal('metric', ['bandwidth']);
170+
171+
$queries = [$equal, $byService, $interval, $byPath];
172+
173+
$extracted = UsageQuery::extractGroupBy($queries);
174+
175+
$this->assertCount(2, $extracted);
176+
$this->assertEquals('service', $extracted[0]->getAttribute());
177+
$this->assertEquals('path', $extracted[1]->getAttribute());
178+
}
179+
180+
public function testExtractGroupByReturnsEmptyWhenAbsent(): void
181+
{
182+
$queries = [
183+
Query::equal('metric', ['bandwidth']),
184+
UsageQuery::groupByInterval('time', '1h'),
185+
];
186+
187+
$this->assertSame([], UsageQuery::extractGroupBy($queries));
188+
}
189+
190+
public function testRemoveGroupBy(): void
191+
{
192+
$byService = UsageQuery::groupBy('service');
193+
$byPath = UsageQuery::groupBy('path');
194+
$interval = UsageQuery::groupByInterval('time', '1h');
195+
$equal = Query::equal('metric', ['bandwidth']);
196+
197+
$queries = [$equal, $byService, $interval, $byPath];
198+
199+
$remaining = UsageQuery::removeGroupBy($queries);
200+
201+
$this->assertCount(2, $remaining);
202+
foreach ($remaining as $query) {
203+
$this->assertNotEquals(UsageQuery::TYPE_GROUP_BY, $query->getMethod());
204+
}
205+
}
206+
207+
public function testGroupByParseRoundTrip(): void
208+
{
209+
$json = json_encode([
210+
'method' => UsageQuery::TYPE_GROUP_BY,
211+
'attribute' => 'service',
212+
'values' => [],
213+
]);
214+
$this->assertIsString($json);
215+
216+
$parsed = UsageQuery::parse($json);
217+
218+
$this->assertEquals(UsageQuery::TYPE_GROUP_BY, $parsed->getMethod());
219+
$this->assertEquals('service', $parsed->getAttribute());
220+
$this->assertEquals([], $parsed->getValues());
221+
}
222+
223+
public function testExtractGroupByFromParsedQuery(): void
224+
{
225+
// Queries created via Query::parse() are base Query objects, not UsageQuery.
226+
$parsedGroupBy = new Query(UsageQuery::TYPE_GROUP_BY, 'service', []);
227+
$equal = Query::equal('metric', ['bandwidth']);
228+
229+
$extracted = UsageQuery::extractGroupBy([$equal, $parsedGroupBy]);
230+
231+
$this->assertCount(1, $extracted);
232+
$this->assertEquals('service', $extracted[0]->getAttribute());
233+
}
134234
}

0 commit comments

Comments
 (0)