Skip to content

Commit 5e78b20

Browse files
Merge pull request #215 from keboola/roman/dmd-265-add-labes
Roman/dmd 265 add labes
2 parents c62a534 + 20f183f commit 5e78b20

6 files changed

Lines changed: 335 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ You must have the `resourcemanager.folders.create` permission for the organizati
275275
# run in provisioning/local/BigQuery folder
276276
terraform init
277277

278-
terraform apply -var organization_id=<your-org-id> -var backend_prefix=<your_backend_prefix> -var billing_account_id=<billing_account_id>
279-
# and enter name for your backend prefix for example your name, all resources will create with this prefx
278+
terraform apply -var folder_id=<your-org-id> -var backend_prefix=<your_backend_prefix> -var billing_account_id=<billing_account_id>
279+
# and enter name for your backend prefix for example your name, all resources will create with this prefix
280280
```
281281

282282
After terraform apply ends go to the service project in folder created by terraform.

src/Connection/Bigquery/BigQueryClientWrapper.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Google\Cloud\BigQuery\Job;
99
use Google\Cloud\BigQuery\JobConfigurationInterface;
1010
use Google\Cloud\BigQuery\QueryResults;
11+
use InvalidArgumentException;
1112
use Retry\BackOff\BackOffPolicyInterface;
1213
use Retry\BackOff\ExponentialBackOffPolicy;
1314
use Retry\BackOff\ExponentialRandomBackOffPolicy;
@@ -18,13 +19,17 @@ class BigQueryClientWrapper extends BigQueryClient
1819
{
1920
private BackOffPolicyInterface $backOffPolicy;
2021

22+
private readonly QueryTags $queryTags;
2123
/**
2224
* @inheritdoc
2325
* @param array<mixed> $config
26+
* @param array<string, string> $queryTags
27+
* @throws InvalidArgumentException if the value is invalid
2428
*/
2529
public function __construct(
2630
array $config = [],
27-
readonly string $runId = '',
31+
string $runId = '',
32+
array $queryTags = [],
2833
BackOffPolicyInterface|null $backOffPolicy = null,
2934
) {
3035
parent::__construct($config);
@@ -37,16 +42,22 @@ public function __construct(
3742
} else {
3843
$this->backOffPolicy = $backOffPolicy;
3944
}
45+
46+
if ($runId !== '') {
47+
$queryTags[QueryTagKey::RUN_ID->value] = $runId;
48+
}
49+
$this->queryTags = new QueryTags($queryTags);
4050
}
4151

4252
/**
4353
* @param array<mixed> $options
4454
*/
4555
public function runQuery(JobConfigurationInterface $query, array $options = []): QueryResults
4656
{
47-
if ($this->runId !== '' && method_exists($query, 'labels')) {
48-
$query = $query->labels(['run_id' => $this->runId]);
57+
if (count($this->queryTags->toArray()) !== 0 && method_exists($query, 'labels')) {
58+
$query = $query->labels($this->queryTags->toArray());
4959
}
60+
5061
return $this->runJob($query, $options)
5162
->queryResults();
5263
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Keboola\TableBackendUtils\Connection\Bigquery;
6+
7+
use InvalidArgumentException;
8+
9+
enum QueryTagKey: string
10+
{
11+
case BRANCH_ID = 'branch_id';
12+
case RUN_ID = 'run_id';
13+
14+
/**
15+
* Validates if the given string is a valid tag key
16+
* @throws InvalidArgumentException if the key is invalid
17+
*/
18+
public static function validateKey(string $key): void
19+
{
20+
$validKeys = array_map(fn(self $case) => $case->value, self::cases());
21+
if (!in_array($key, $validKeys, true)) {
22+
throw new InvalidArgumentException(sprintf(
23+
'Invalid query tag key "%s". Valid keys are: %s',
24+
$key,
25+
implode(', ', $validKeys),
26+
));
27+
}
28+
}
29+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Keboola\TableBackendUtils\Connection\Bigquery;
6+
7+
use InvalidArgumentException;
8+
9+
/**
10+
* Class for managing and validating BigQuery query labels
11+
*/
12+
class QueryTags
13+
{
14+
private const MAX_LENGTH = 63;
15+
// Pattern allows lowercase letters (including international), numbers, underscores, and dashes
16+
private const VALUE_PATTERN = '/^[\p{Ll}0-9_-]*$/u';
17+
18+
/** @var array<string, string> */
19+
private array $queryTags = [];
20+
21+
/**
22+
* @param array<string, string> $tags
23+
*/
24+
public function __construct(array $tags = [])
25+
{
26+
foreach ($tags as $key => $value) {
27+
$this->addTag($key, $value);
28+
}
29+
}
30+
31+
/** @throws InvalidArgumentException if the value is invalid */
32+
public function addTag(string $key, string $value): self
33+
{
34+
// Validate the key using QueryTagKey enum
35+
QueryTagKey::validateKey($key);
36+
37+
// Validate the value format
38+
$this->validateQueryTagValue($key, $value);
39+
40+
$this->queryTags[$key] = $value;
41+
return $this;
42+
}
43+
44+
/**
45+
* Validates if the given value follows BigQuery query label requirements:
46+
* - Can be no longer than 63 characters
47+
* - Can only contain lowercase letters (including international), numeric characters, underscores and dashes
48+
* - Empty values are allowed
49+
*
50+
* @throws InvalidArgumentException if the value is invalid
51+
*/
52+
private function validateQueryTagValue(string $key, string $value): void
53+
{
54+
if (strlen($value) > self::MAX_LENGTH) {
55+
throw new InvalidArgumentException(sprintf(
56+
'Query tag value "%s" for key "%s" is too long. Maximum length is %d characters.',
57+
$value,
58+
$key,
59+
self::MAX_LENGTH,
60+
));
61+
}
62+
63+
if ($value !== '' && !preg_match(self::VALUE_PATTERN, $value)) {
64+
throw new InvalidArgumentException(sprintf(
65+
'Invalid query tag value "%s" for key "%s". Values can only contain' .
66+
' lowercase letters (including international characters), numbers, underscores and dashes.',
67+
$value,
68+
$key,
69+
));
70+
}
71+
}
72+
73+
public function isEmpty(): bool
74+
{
75+
return empty($this->queryTags);
76+
}
77+
78+
/**
79+
* @return array<string, string>
80+
*/
81+
public function toArray(): array
82+
{
83+
return $this->queryTags;
84+
}
85+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Keboola\TableBackendUtils\Functional\Bigquery\Connection;
6+
7+
use GuzzleHttp\Client;
8+
use Keboola\TableBackendUtils\Connection\Bigquery\BigQueryClientHandler;
9+
use Keboola\TableBackendUtils\Connection\Bigquery\BigQueryClientWrapper;
10+
use Keboola\TableBackendUtils\Connection\Bigquery\Retry;
11+
use Psr\Log\NullLogger;
12+
use Tests\Keboola\TableBackendUtils\Functional\Bigquery\BigqueryBaseCase;
13+
14+
class QueryTagsTest extends BigqueryBaseCase
15+
{
16+
public function setUp(): void
17+
{
18+
parent::setUp();
19+
}
20+
21+
public function testRunIdAndBranchIdQueryTagExists(): void
22+
{
23+
$bqClient = new BigQueryClientWrapper(
24+
[
25+
'keyFile' => $this->getCredentials(),
26+
'httpHandler' => new BigQueryClientHandler(new Client()),
27+
'restRetryFunction' => Retry::getRestRetryFunction(new NullLogger(), true),
28+
],
29+
'e2e-utils-lib',
30+
[
31+
'branch_id' => '1234',
32+
],
33+
);
34+
35+
$query = $bqClient->query('SELECT 1');
36+
$queryResults = $bqClient->runQuery($query);
37+
38+
// Get job info and verify the run_id and branch_id label
39+
$job = $queryResults->job();
40+
$info = $job->info();
41+
42+
$this->assertArrayHasKey('configuration', $info);
43+
$this->assertArrayHasKey('labels', $info['configuration']);
44+
$this->assertArrayHasKey('run_id', $info['configuration']['labels']);
45+
$this->assertEquals('e2e-utils-lib', $info['configuration']['labels']['run_id']);
46+
47+
$this->assertArrayHasKey('branch_id', $info['configuration']['labels']);
48+
$this->assertEquals('1234', $info['configuration']['labels']['branch_id']);
49+
}
50+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Keboola\TableBackendUtils\Unit\Connection\Bigquery;
6+
7+
use InvalidArgumentException;
8+
use Keboola\TableBackendUtils\Connection\Bigquery\QueryTagKey;
9+
use Keboola\TableBackendUtils\Connection\Bigquery\QueryTags;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class QueryTagsTest extends TestCase
13+
{
14+
public function testEmptyQueryTags(): void
15+
{
16+
$queryTags = new QueryTags();
17+
$this->assertTrue($queryTags->isEmpty());
18+
$this->assertEmpty($queryTags->toArray());
19+
}
20+
21+
public function testValidQueryTagsInConstructor(): void
22+
{
23+
$queryTags = new QueryTags([
24+
QueryTagKey::BRANCH_ID->value => 'test-branch',
25+
]);
26+
27+
$this->assertFalse($queryTags->isEmpty());
28+
$this->assertEquals(
29+
['branch_id' => 'test-branch'],
30+
$queryTags->toArray(),
31+
);
32+
}
33+
34+
public function testValidQueryTagsAddition(): void
35+
{
36+
$queryTags = new QueryTags();
37+
$queryTags->addTag(QueryTagKey::BRANCH_ID->value, 'test-branch');
38+
39+
$this->assertFalse($queryTags->isEmpty());
40+
$this->assertEquals(
41+
['branch_id' => 'test-branch'],
42+
$queryTags->toArray(),
43+
);
44+
}
45+
46+
public function testInvalidQueryTagsInConstructor(): void
47+
{
48+
$this->expectException(InvalidArgumentException::class);
49+
$this->expectExceptionMessage('Invalid query tag key "invalid_key". Valid keys are: branch_id');
50+
51+
new QueryTags([
52+
'invalid_key' => 'some-value',
53+
]);
54+
}
55+
56+
public function testInvalidQueryTagsAddition(): void
57+
{
58+
$this->expectException(InvalidArgumentException::class);
59+
$this->expectExceptionMessage('Invalid query tag key "invalid_key". Valid keys are: branch_id');
60+
61+
$queryTags = new QueryTags();
62+
$queryTags->addTag('invalid_key', 'some-value');
63+
}
64+
65+
public function testChainedQueryTagsAddition(): void
66+
{
67+
$queryTags = new QueryTags();
68+
$queryTags
69+
->addTag(QueryTagKey::BRANCH_ID->value, 'test-branch-1')
70+
->addTag(QueryTagKey::BRANCH_ID->value, 'test-branch-2'); // Override previous value
71+
72+
$this->assertEquals(
73+
['branch_id' => 'test-branch-2'],
74+
$queryTags->toArray(),
75+
);
76+
}
77+
78+
/**
79+
* @dataProvider validQueryTagsValuesProvider
80+
*/
81+
public function testValidQueryTagsValues(string $value): void
82+
{
83+
$queryTags = new QueryTags();
84+
$queryTags->addTag(QueryTagKey::BRANCH_ID->value, $value);
85+
$this->assertEquals($value, $queryTags->toArray()[QueryTagKey::BRANCH_ID->value]);
86+
}
87+
88+
/**
89+
* @return array<string, array{string}>
90+
*/
91+
public function validQueryTagsValuesProvider(): array
92+
{
93+
return [
94+
'empty value' => [''],
95+
'simple value' => ['test'],
96+
'with numbers' => ['test123'],
97+
'with underscore' => ['test_branch'],
98+
'with dash' => ['test-branch'],
99+
'complex value' => ['my-test-branch-123'],
100+
'single letter' => ['a'],
101+
'starts with number' => ['123-test'],
102+
'starts with underscore' => ['_test'],
103+
'starts with dash' => ['-test'],
104+
'international characters' => ['čeština-test'],
105+
'international single char' => ['ě'],
106+
'mixed international' => ['test-über-123'],
107+
];
108+
}
109+
110+
/**
111+
* @dataProvider invalidQueryTagsValuesProvider
112+
*/
113+
public function testInvalidQueryTagsValues(string $value, string $expectedMessage): void
114+
{
115+
$this->expectException(InvalidArgumentException::class);
116+
$this->expectExceptionMessage($expectedMessage);
117+
118+
$queryTags = new QueryTags();
119+
$queryTags->addTag(QueryTagKey::BRANCH_ID->value, $value);
120+
}
121+
122+
/**
123+
* @return array<string, array{string, string}>
124+
*/
125+
public function invalidQueryTagsValuesProvider(): array
126+
{
127+
return [
128+
'uppercase letters' => [
129+
'Test-Branch',
130+
'Invalid query tag value "Test-Branch" for key "branch_id". Values can only contain lowercase letters'
131+
.' (including international characters), numbers, underscores and dashes.',
132+
],
133+
'special characters' => [
134+
'test@branch',
135+
'Invalid query tag value "test@branch" for key "branch_id". Values can only contain lowercase letters'
136+
.' (including international characters), numbers, underscores and dashes.',
137+
],
138+
'too long' => [
139+
str_repeat('a', 64),
140+
'Query tag value "' . str_repeat('a', 64)
141+
. '" for key "branch_id" is too long. Maximum length is 63 characters.',
142+
],
143+
'uppercase international' => [
144+
'ÜBER-test',
145+
'Invalid query tag value "ÜBER-test" for key "branch_id". Values can only contain lowercase letters'
146+
.' (including international characters), numbers, underscores and dashes.',
147+
],
148+
'special international' => [
149+
'test-€-symbol',
150+
'Invalid query tag value "test-€-symbol" for key "branch_id". Values can only contain lowercase letters'
151+
.' (including international characters), numbers, underscores and dashes.',
152+
],
153+
];
154+
}
155+
}

0 commit comments

Comments
 (0)