Skip to content

Commit a1f6930

Browse files
feat(BigQuery): add Stateless Query support (#9022)
1 parent 2cc507c commit a1f6930

5 files changed

Lines changed: 427 additions & 53 deletions

File tree

BigQuery/src/BigQueryClient.php

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,44 @@ public function runQuery(JobConfigurationInterface $query, array $options = [])
417417
], $options);
418418
$queryResultsOptions['initialTimeoutMs'] = 10000;
419419

420-
$queryResults = $this->startQuery(
421-
$query,
422-
$options
423-
)->queryResults($queryResultsOptions + $options);
420+
if ($query instanceof QueryJobConfiguration && $query->isStateless()) {
421+
$queryRequest = $query->toQueryRequest();
422+
423+
// The flattened notation does not work for POST request without special handling.
424+
// Propagate it for backwards compatibility.
425+
if (isset($queryResultsOptions['formatOptions.useInt64Timestamp'])) {
426+
$useInt64 = $this->pluck('formatOptions.useInt64Timestamp', $queryResultsOptions, false);
427+
$queryResultsOptions['formatOptions']['useInt64Timestamp'] = $useInt64;
428+
}
429+
430+
$statelessArgs = $queryRequest + $queryResultsOptions + [
431+
'projectId' => $this->projectId
432+
] + $options;
433+
434+
if (!isset($statelessArgs['timeoutMs'])) {
435+
$statelessArgs['timeoutMs'] = $statelessArgs['initialTimeoutMs'];
436+
}
437+
438+
$response = $this->connection->query($statelessArgs);
439+
440+
if ($response['jobComplete'] ?? false) {
441+
return new QueryResults(
442+
$this->connection,
443+
'',
444+
$this->projectId,
445+
$response,
446+
$this->mapper,
447+
$this->createJob([], ''), // create an empty job
448+
$queryResultsOptions
449+
);
450+
}
451+
452+
$job = $this->createJob($response, $response['jobReference']['jobId']);
453+
} else {
454+
$job = $this->startQuery($query, $options);
455+
}
456+
457+
$queryResults = $job->queryResults($queryResultsOptions + $options);
424458
$queryResults->waitUntilComplete();
425459
return $queryResults;
426460
}
@@ -772,12 +806,17 @@ public function startJob(JobConfigurationInterface $config, array $options = [])
772806
$response = $this->connection->insertJob($config);
773807
}
774808

809+
return $this->createJob($response, $config['jobReference']['jobId']);
810+
}
811+
812+
private function createJob(array $info, string $jobId)
813+
{
775814
return new Job(
776815
$this->connection,
777-
$config['jobReference']['jobId'],
816+
$jobId,
778817
$this->projectId,
779818
$this->mapper,
780-
$response
819+
$info
781820
);
782821
}
783822

BigQuery/src/QueryJobConfiguration.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ class QueryJobConfiguration implements JobConfigurationInterface
3737
{
3838
use JobConfigurationTrait;
3939

40+
private const JOB_CREATION_MODE_OPTIONAL = 'JOB_CREATION_OPTIONAL';
41+
4042
/**
4143
* @var ValueMapper Maps values between PHP and BigQuery.
4244
*/
4345
private $mapper;
46+
private bool $isJobIdGenerated = false;
4447

4548
/**
4649
* @param ValueMapper $mapper Maps values between PHP and BigQuery.
@@ -58,6 +61,13 @@ public function __construct(
5861
$this->mapper = $mapper;
5962
$this->jobConfigurationProperties($projectId, $config, $location);
6063

64+
if (!isset($config['jobReference']['jobId'])) {
65+
// If the user did not submit a jobId to the configuration array, the library will create a JobId before
66+
// the request is sent. This is used to keep track if it is the user who set the JobId or the library
67+
/// for stateless queries.
68+
$this->isJobIdGenerated = true;
69+
}
70+
6171
if (!isset($this->config['configuration']['query']['useLegacySql'])) {
6272
$this->config['configuration']['query']['useLegacySql'] = false;
6373
}
@@ -593,4 +603,89 @@ public function writeDisposition($writeDisposition)
593603

594604
return $this;
595605
}
606+
607+
/**
608+
* Returns true if the current configuration is compatible with the stateless query API.
609+
* False if not
610+
*
611+
* @internal
612+
* @return bool
613+
*/
614+
public function isStateless(): bool
615+
{
616+
$config = $this->config;
617+
$queryConfig = $config['configuration']['query'];
618+
619+
if (isset($queryConfig['destinationTable']) ||
620+
isset($queryConfig['tableDefinitions']) ||
621+
isset($queryConfig['createDisposition']) ||
622+
isset($queryConfig['writeDisposition']) ||
623+
($queryConfig['useLegacySql'] ?? false) ||
624+
isset($queryConfig['maximumBillingTier']) ||
625+
isset($queryConfig['timePartitioning']) ||
626+
isset($queryConfig['rangePartitioning']) ||
627+
isset($queryConfig['clustering']) ||
628+
isset($queryConfig['destinationEncryptionConfiguration']) ||
629+
isset($queryConfig['schemaUpdateOptions']) ||
630+
isset($queryConfig['jobTimeoutMs'])
631+
) {
632+
return false;
633+
}
634+
635+
if (isset($queryConfig['priority']) && $queryConfig['priority'] !== 'INTERACTIVE') {
636+
return false;
637+
}
638+
639+
if ($config['configuration']['dryRun'] ?? false) {
640+
return false;
641+
}
642+
643+
// Creating a jobConfiguration from the library sets the JobId always meaning we do not have a way
644+
// to determine if this jobId was set by the user or our library.
645+
// We check if this was autogenerated to circumvent this issue.
646+
if (isset($config['jobReference']['jobId']) && !$this->isJobIdGenerated()) {
647+
return false;
648+
}
649+
650+
return true;
651+
}
652+
653+
/**
654+
* Returns an array representation of a QueryRequest:
655+
* [QueryRequest](https://docs.cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query#QueryRequest)
656+
*
657+
* @return array
658+
*/
659+
public function toQueryRequest(): array
660+
{
661+
$config = $this->config;
662+
$queryConfig = $config['configuration']['query'];
663+
664+
return [
665+
'query' => $queryConfig['query'],
666+
'maxResults' => $queryConfig['maxResults'] ?? null,
667+
'defaultDataset' => $queryConfig['defaultDataset'] ?? null,
668+
'timeoutMs' => $queryConfig['timeoutMs'] ?? null,
669+
'useQueryCache' => $queryConfig['useQueryCache'] ?? null,
670+
'useLegacySql' => false,
671+
'queryParameters' => $queryConfig['queryParameters'] ?? null,
672+
'parameterMode' => $queryConfig['parameterMode'] ?? null,
673+
'labels' => $config['configuration']['labels'] ?? null,
674+
'createSession' => $queryConfig['createSession'] ?? null,
675+
'maximumBytesBilled' => $queryConfig['maximumBytesBilled'] ?? null,
676+
'location' => $config['jobReference']['location'] ?? null,
677+
'requestId' => $config['jobReference']['jobId'],
678+
'jobCreationMode' => self::JOB_CREATION_MODE_OPTIONAL
679+
];
680+
}
681+
682+
/**
683+
* Returns if the JobId was generated by the JobConfigurationTrait
684+
*
685+
* @return bool
686+
*/
687+
private function isJobIdGenerated(): bool
688+
{
689+
return $this->isJobIdGenerated;
690+
}
596691
}

BigQuery/src/QueryResults.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class QueryResults implements \IteratorAggregate
6868
* @var array Default options to be used for calls to get query results.
6969
*/
7070
private $queryResultsOptions;
71+
private bool $isStateless = false;
7172

7273
/**
7374
* @param ConnectionInterface $connection Represents a connection to
@@ -102,6 +103,7 @@ public function __construct(
102103
? $info['jobReference']['location']
103104
: $job->identity()['location']
104105
];
106+
$this->isStateless = empty($jobId);
105107
$this->mapper = $mapper;
106108
$this->queryResultsOptions = $queryResultsOptions;
107109
}
@@ -292,6 +294,10 @@ public function info()
292294
*/
293295
public function reload(array $options = [])
294296
{
297+
if ($this->isStateless) {
298+
return $this->info;
299+
}
300+
295301
return $this->info = $this->connection->getQueryResults(
296302
$options + $this->identity
297303
);

BigQuery/tests/Snippet/BigQueryClientTest.php

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function testRunQuery()
154154
{
155155
$snippet = $this->snippetFromMethod(BigQueryClient::class, 'runQuery');
156156
$snippet->addLocal('bigQuery', $this->client);
157-
$this->connection->insertJob(Argument::any())
157+
$this->connection->query(Argument::any())
158158
->shouldBeCalled()
159159
->willReturn([
160160
'jobComplete' => false,
@@ -179,36 +179,42 @@ public function testRunQueryWithNamedParameters()
179179
$expectedQuery = 'SELECT commit FROM `bigquery-public-data.github_repos.commits`' .
180180
'WHERE author.date < @date AND message = @message LIMIT 100';
181181
$this->connection
182-
->insertJob([
183-
'projectId' => self::PROJECT_ID,
184-
'jobReference' => ['projectId' => self::PROJECT_ID, 'jobId' => self::JOB_ID],
185-
'configuration' => [
186-
'query' => [
187-
'parameterMode' => 'named',
188-
'useLegacySql' => false,
189-
'queryParameters' => [
190-
[
191-
'name' => 'date',
192-
'parameterType' => [
193-
'type' => 'TIMESTAMP'
194-
],
195-
'parameterValue' => [
196-
'value' => '1980-01-01 12:15:00.000000+00:00'
197-
]
198-
],
199-
[
200-
'name' => 'message',
201-
'parameterType' => [
202-
'type' => 'STRING'
203-
],
204-
'parameterValue' => [
205-
'value' => 'A commit message.'
206-
]
207-
]
182+
->query([
183+
"query"=> $expectedQuery,
184+
"maxResults"=> null,
185+
"defaultDataset"=> null,
186+
"timeoutMs"=> 10000,
187+
"useQueryCache"=> null,
188+
"useLegacySql"=> false,
189+
"queryParameters"=> [
190+
[
191+
"parameterType"=> [
192+
"type"=> "TIMESTAMP"
193+
],
194+
"parameterValue"=> [
195+
"value"=> "1980-01-01 12:15:00.000000+00:00"
196+
],
197+
"name"=> "date"
198+
],
199+
[
200+
"parameterType"=> [
201+
"type"=> "STRING"
202+
],
203+
"parameterValue"=> [
204+
"value"=> "A commit message."
208205
],
209-
'query' => $expectedQuery
206+
"name"=> "message"
210207
]
211-
]
208+
],
209+
"parameterMode"=> "named",
210+
"labels"=> null,
211+
"createSession"=> null,
212+
"maximumBytesBilled"=> null,
213+
"location"=> null,
214+
"requestId"=> "myJobId",
215+
"jobCreationMode"=> "JOB_CREATION_OPTIONAL",
216+
"initialTimeoutMs"=> 10000,
217+
"projectId"=> "my-awesome-project"
212218
])
213219
->shouldBeCalledTimes(1)
214220
->willReturn([
@@ -233,26 +239,32 @@ public function testRunQueryWithPositionalParameters()
233239
$snippet->addLocal('bigQuery', $this->client);
234240
$expectedQuery = 'SELECT commit FROM `bigquery-public-data.github_repos.commits` WHERE message = ? LIMIT 100';
235241
$this->connection
236-
->insertJob([
237-
'projectId' => self::PROJECT_ID,
238-
'jobReference' => ['projectId' => self::PROJECT_ID, 'jobId' => self::JOB_ID],
239-
'configuration' => [
240-
'query' => [
241-
'parameterMode' => 'positional',
242-
'useLegacySql' => false,
243-
'queryParameters' => [
244-
[
245-
'parameterType' => [
246-
'type' => 'STRING'
247-
],
248-
'parameterValue' => [
249-
'value' => 'A commit message.'
250-
]
251-
]
242+
->query([
243+
"query"=> "SELECT commit FROM `bigquery-public-data.github_repos.commits` WHERE message = ? LIMIT 100",
244+
"maxResults"=> null,
245+
"defaultDataset"=> null,
246+
"timeoutMs"=> 10000,
247+
"useQueryCache"=> null,
248+
"useLegacySql"=> false,
249+
"queryParameters"=> [
250+
[
251+
"parameterType"=> [
252+
"type"=> "STRING"
252253
],
253-
'query' => $expectedQuery
254+
"parameterValue"=> [
255+
"value"=> "A commit message."
256+
]
254257
]
255-
]
258+
],
259+
"parameterMode"=> "positional",
260+
"labels"=> null,
261+
"createSession"=> null,
262+
"maximumBytesBilled"=> null,
263+
"location"=> null,
264+
"requestId"=> "myJobId",
265+
"jobCreationMode"=> "JOB_CREATION_OPTIONAL",
266+
"initialTimeoutMs"=> 10000,
267+
"projectId"=> "my-awesome-project"
256268
])
257269
->shouldBeCalledTimes(1)
258270
->willReturn([

0 commit comments

Comments
 (0)