Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/Audit/Adapter/ClickHouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class ClickHouse extends SQL
'resourceType',
'country',
'sdk',
// premium geo (autonomousSystemNumber is intentionally NOT low-cardinality:
// ~100k ASNs globally can exceed ClickHouse's LowCardinality sweet spot)
'continentCode',
'subdivisions',
'connectionType',
'connectionUsageType',
];

/**
Expand Down Expand Up @@ -482,6 +488,97 @@ public function getAttributes(): array
'array' => false,
'filters' => [],
],
// premium geo
[
'$id' => 'city',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'continentCode',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'subdivisions',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'isp',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'autonomousSystemNumber',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'autonomousSystemOrganization',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'connectionType',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'connectionUsageType',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'connectionOrganization',
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => false,
'default' => null,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
Expand Down
135 changes: 134 additions & 1 deletion tests/Audit/Adapter/ClickHouseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,15 +536,148 @@ public function testClickHouseAdapterAttributes(): void
'teamId',
'teamInternalId',
'hostname',
// premium geo
'city',
'continentCode',
'subdivisions',
'isp',
'autonomousSystemNumber',
'autonomousSystemOrganization',
'connectionType',
'connectionUsageType',
'connectionOrganization',
// sdk
'sdk',
'sdkVersion'
'sdkVersion',
];

foreach ($expectedAttributes as $expected) {
$this->assertContains($expected, $attributeIds, "Attribute '{$expected}' not found in ClickHouse adapter");
}
}

/**
* Test that premium geo attributes are all optional String columns.
*/
public function testPremiumGeoAttributesAreOptionalStrings(): void
{
$adapter = new ClickHouse(
host: 'clickhouse',
username: 'default',
password: 'clickhouse'
);

$attributes = $adapter->getAttributes();
$byId = [];
foreach ($attributes as $attribute) {
$byId[$attribute['$id']] = $attribute;
}

$geoColumns = [
'city',
'continentCode',
'subdivisions',
'isp',
'autonomousSystemNumber',
'autonomousSystemOrganization',
'connectionType',
'connectionUsageType',
'connectionOrganization',
];

foreach ($geoColumns as $column) {
$this->assertArrayHasKey($column, $byId, "Premium geo attribute '{$column}' not found");
$this->assertEquals(\Utopia\Database\Database::VAR_STRING, $byId[$column]['type'], "'{$column}' should be a string");
$this->assertFalse($byId[$column]['required'], "'{$column}' should be optional");
$this->assertFalse($byId[$column]['array'], "'{$column}' should not be an array");
}
}

/**
* Test that premium geo columns get the correct ClickHouse type:
* low-cardinality dimensions use LowCardinality(Nullable(String)), while
* high-cardinality ones stay plain Nullable(String).
*/
public function testPremiumGeoColumnTypes(): void
{
$adapter = new ClickHouse(
host: 'clickhouse',
username: 'default',
password: 'clickhouse'
);

$method = new \ReflectionMethod($adapter, 'getColumnDefinition');
$method->setAccessible(true);

$lowCardinality = [
'continentCode',
'subdivisions',
'connectionType',
'connectionUsageType',
];
foreach ($lowCardinality as $column) {
$definition = $method->invoke($adapter, $column);
$this->assertEquals("{$column} LowCardinality(Nullable(String))", $definition);
}

// autonomousSystemNumber is high-cardinality (~100k ASNs) so it is a
// plain Nullable(String), not LowCardinality.
$highCardinality = [
'city',
'isp',
'autonomousSystemNumber',
'autonomousSystemOrganization',
'connectionOrganization',
];
foreach ($highCardinality as $column) {
$definition = $method->invoke($adapter, $column);
$this->assertEquals("{$column} Nullable(String)", $definition);
}
}

/**
* Premium geo values must round-trip through a real write/read cycle:
* write a log carrying all 9 geo fields and read it back unchanged. This
* proves the columns are actually created, written and selected (not just
* present in the schema definition).
*/
public function testPremiumGeoRoundTrip(): void
{
$actorId = 'geo-actor-' . uniqid('', true);
$geo = [
'city' => 'Mountain View',
'continentCode' => 'NA',
'subdivisions' => 'California',
'isp' => 'Google',
'autonomousSystemNumber' => '15169',
'autonomousSystemOrganization' => 'GOOGLE',
'connectionType' => 'cable',
'connectionUsageType' => 'residential',
'connectionOrganization' => 'Google LLC',
];

$batchEvents = [array_merge([
'actorId' => $actorId,
'event' => 'geo.roundtrip',
'resource' => 'document/geo-1',
'userAgent' => 'RoundTrip/1.0',
'ip' => '8.8.8.8',
'data' => [],
'time' => \Utopia\Database\DateTime::formatTz(\Utopia\Database\DateTime::now()) ?? '',
], $geo)];

$batchEvents = $this->applyRequiredAttributesToBatch($batchEvents);
$this->assertTrue($this->audit->logBatch($batchEvents));

$logs = $this->audit->getLogsByUser($actorId);
$this->assertGreaterThan(0, count($logs), 'geo round-trip log was not persisted');

$log = $logs[0];
foreach ($geo as $key => $expected) {
$this->assertSame($expected, $log->getAttribute($key), "premium geo '{$key}' did not round-trip");
}
}

/**
* Test that ClickHouse adapter has all required indexes
*/
Expand Down
Loading