diff --git a/BigQuery/src/BigQueryClient.php b/BigQuery/src/BigQueryClient.php index df14869c75f3..e2be642e15c2 100644 --- a/BigQuery/src/BigQueryClient.php +++ b/BigQuery/src/BigQueryClient.php @@ -34,6 +34,8 @@ use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use function PHPUnit\Framework\isNull; + /** * Google Cloud BigQuery allows you to create, manage, share and query data. * Find more information at the @@ -417,6 +419,45 @@ public function runQuery(JobConfigurationInterface $query, array $options = []) ], $options); $queryResultsOptions['initialTimeoutMs'] = 10000; + // Check if we can build a query Request + $queryRequest = StatelessJobConfiguration::getQueryRequest($query); + + if (!is_null($queryRequest)) { + if (isset($queryResultsOptions['formatOptions.useInt64Timestamp'])) { + $useInt64 = $this->pluck('formatOptions.useInt64Timestamp', $queryResultsOptions, false); + + if (!isset($queryResultsOptions['formatOptions']) || !is_array($queryResultsOptions['formatOptions'])) { + $queryResultsOptions['formatOptions'] = []; + } + + $queryResultsOptions['formatOptions']['useInt64Timestamp'] = $useInt64; + } + + $statelessArgs = $queryRequest + $queryResultsOptions + [ + 'projectId' => $this->projectId + ] + $options; + + if (!isset($statelessArgs['timeoutMs'])) { + $statelessArgs['timeoutMs'] = $statelessArgs['initialTimeoutMs']; + } + + $statelessResponse = $this->connection->query($statelessArgs); + + $queryResults = QueryResults::fromStatelessQuery( + $this->connection, + $this->projectId, + $statelessResponse, + $this->mapper, + $queryResultsOptions + $options + ); + + if (!$queryResults->isComplete()) { + $queryResults->waitUntilComplete(); + } + + return $queryResults; + } + $queryResults = $this->startQuery( $query, $options diff --git a/BigQuery/src/Connection/Rest.php b/BigQuery/src/Connection/Rest.php index 693d154d7bb6..90f77d8888a5 100644 --- a/BigQuery/src/Connection/Rest.php +++ b/BigQuery/src/Connection/Rest.php @@ -414,6 +414,11 @@ public function setTableIamPolicy(array $args = []) return $this->send('tables', 'setIamPolicy', $args); } + public function statelessQuery(array $args = []) + { + return $this->send('jobs', 'query', $args); + } + /** * @param array $args * @return array diff --git a/BigQuery/src/JobConfigurationTrait.php b/BigQuery/src/JobConfigurationTrait.php index b4775235ef07..14fa99028899 100644 --- a/BigQuery/src/JobConfigurationTrait.php +++ b/BigQuery/src/JobConfigurationTrait.php @@ -37,6 +37,8 @@ trait JobConfigurationTrait */ private $config = []; + private bool $isJobIdGenerated = false; + /** * Sets shared job configuration properties. * @@ -62,6 +64,9 @@ public function jobConfigurationProperties( if (!isset($this->config['jobReference']['jobId'])) { $this->config['jobReference']['jobId'] = $this->generateJobId(); + + // Used for the Stateless query logic + $this->isJobIdGenerated = true; } } @@ -165,6 +170,11 @@ public function location($location) return $this; } + public function isJobIdGenerated(): bool + { + return $this->isJobIdGenerated; + } + /** * Returns the job config as an array. * diff --git a/BigQuery/src/QueryResults.php b/BigQuery/src/QueryResults.php index 4da695acfe59..ed4d9d87fdd4 100644 --- a/BigQuery/src/QueryResults.php +++ b/BigQuery/src/QueryResults.php @@ -292,6 +292,10 @@ public function info() */ public function reload(array $options = []) { + if (!isset($this->info['jobReference'])) { + return $this->info; + } + return $this->info = $this->connection->getQueryResults( $options + $this->identity ); @@ -371,4 +375,49 @@ public function getIterator() { return $this->rows(); } + + /** + * @param ConnectionInterface $connection Represents a connection to + * BigQuery. This object is created by BigQueryClient, + * and should not be instantiated outside of this client. + * @param string $projectId The project's ID. + * @param array $statelessResponse The query result's metadata. + * @param ValueMapper $mapper Maps values between PHP and BigQuery. + * @param array $queryResultsOptions Default options to be used for calls to + * get query results. See + * [documentation](https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/getQueryResults#query-parameters) + * for available options. + */ + public static function fromStatelessQuery( + ConnectionInterface $connection, + string $projectId, + array $statelessResponse, + ValueMapper $mapper, + array $queryResultsOptions = [] + ): QueryResults { + $jobReference = $statelessResponse['jobReference'] ?? []; + // If jobId is null, it was a stateless request that completed in one request. + $jobId = $jobReference['jobId'] ?? null; + $projectId = $jobReference['projectId'] ?? $projectId; + $location = $jobReference['location'] ?? ($statelessResponse['location'] ?? null); + + $job = new Job( + $connection, + $jobId, + $projectId, + $mapper, + [], + $location + ); + + return new QueryResults( + $connection, + $jobId, + $projectId, + $statelessResponse, + $mapper, + $job, + $queryResultsOptions + ); + } } diff --git a/BigQuery/src/StatelessJobConfiguration.php b/BigQuery/src/StatelessJobConfiguration.php new file mode 100644 index 000000000000..38c7da3f50dd --- /dev/null +++ b/BigQuery/src/StatelessJobConfiguration.php @@ -0,0 +1,111 @@ +|null + */ + public static function getQueryRequest(JobConfigurationInterface $jobConfiguration): array|null + { + $config = $jobConfiguration->toArray(); + $queryConfig = $config['configuration']['query']; + + if ( + isset($queryConfig['destinationTable']) || + isset($queryConfig['tableDefinitions']) || + isset($queryConfig['createDisposition']) || + isset($queryConfig['writeDisposition']) || + ( + isset($queryConfig['priority']) && + $queryConfig['priority'] !== 'INTERACTIVE' + ) || + (isset($queryConfig['useLegacySql']) && $queryConfig['useLegacySql']) || + isset($queryConfig['maximumBillingTier']) || + isset($queryConfig['timePartitioning']) || + isset($queryConfig['rangePartitioning']) || + isset($queryConfig['clustering']) || + isset($queryConfig['destinationEncryptionConfiguration']) || + isset($queryConfig['schemaUpdateOptions']) || + isset($queryConfig['jobTimeoutMs']) || + isset($queryConfig['jobId']) + ) { + return null; + } + + if (isset($config['configuration']['dryRun']) && $config['configuration']['dryRun']) { + return null; + } + + // Creating a jobConfiguration from the library sets the JobId always meaning we do not have a way + // to determine if this jobId was set by the user or our library. + // We check if this was autogenerated to circumvent this issue. + if ( + isset($config['jobReference']['jobId']) && + method_exists($jobConfiguration, 'isJobIdGenerated') && + !$jobConfiguration->isJobIdGenerated() + ) { + return null; + } + + return [ + 'query' => $queryConfig['query'], + 'maxResults' => $queryConfig['maxResults'] ?? null, + 'defaultDataset' => $queryConfig['defaultDataset'] ?? null, + 'timeoutMs' => $queryConfig['timeoutMs'] ?? null, + 'useQueryCache' => $queryConfig['useQueryCache'] ?? null, + 'useLegacySql' => false, + 'queryParameters' => $queryConfig['queryParameters'] ?? null, + 'parameterMode' => $queryConfig['parameterMode'] ?? null, + 'labels' => $config['configuration']['labels'] ?? null, + 'createSession' => $queryConfig['createSession'] ?? null, + 'maximumBytesBilled' => $queryConfig['maximumBytesBilled'] ?? null, + 'location' => $config['jobReference']['location'] ?? null, + 'requestId' => $config['jobReference']['jobId'], + 'jobCreationMode' => self::JOB_CREATION_MODE_OPTIONAL + ]; + } + + /** + * Generate a Job ID. + * + * @return string + */ + protected static function generateJobId() + { + return Uuid::uuid4()->toString(); + } +} diff --git a/BigQuery/tests/Snippet/BigQueryClientTest.php b/BigQuery/tests/Snippet/BigQueryClientTest.php index 06f189299285..a6cff07dd077 100644 --- a/BigQuery/tests/Snippet/BigQueryClientTest.php +++ b/BigQuery/tests/Snippet/BigQueryClientTest.php @@ -154,7 +154,7 @@ public function testRunQuery() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'runQuery'); $snippet->addLocal('bigQuery', $this->client); - $this->connection->insertJob(Argument::any()) + $this->connection->query(Argument::any()) ->shouldBeCalled() ->willReturn([ 'jobComplete' => false, @@ -179,36 +179,42 @@ public function testRunQueryWithNamedParameters() $expectedQuery = 'SELECT commit FROM `bigquery-public-data.github_repos.commits`' . 'WHERE author.date < @date AND message = @message LIMIT 100'; $this->connection - ->insertJob([ - 'projectId' => self::PROJECT_ID, - 'jobReference' => ['projectId' => self::PROJECT_ID, 'jobId' => self::JOB_ID], - 'configuration' => [ - 'query' => [ - 'parameterMode' => 'named', - 'useLegacySql' => false, - 'queryParameters' => [ - [ - 'name' => 'date', - 'parameterType' => [ - 'type' => 'TIMESTAMP' - ], - 'parameterValue' => [ - 'value' => '1980-01-01 12:15:00.000000+00:00' - ] - ], - [ - 'name' => 'message', - 'parameterType' => [ - 'type' => 'STRING' - ], - 'parameterValue' => [ - 'value' => 'A commit message.' - ] - ] + ->query([ + "query"=> "SELECT commit FROM `bigquery-public-data.github_repos.commits`WHERE author.date < @date AND message = @message LIMIT 100", + "maxResults"=> null, + "defaultDataset"=> null, + "timeoutMs"=> 10000, + "useQueryCache"=> null, + "useLegacySql"=> false, + "queryParameters"=> [ + [ + "parameterType"=> [ + "type"=> "TIMESTAMP" + ], + "parameterValue"=> [ + "value"=> "1980-01-01 12:15:00.000000+00:00" + ], + "name"=> "date" + ], + [ + "parameterType"=> [ + "type"=> "STRING" + ], + "parameterValue"=> [ + "value"=> "A commit message." ], - 'query' => $expectedQuery + "name"=> "message" ] - ] + ], + "parameterMode"=> "named", + "labels"=> null, + "createSession"=> null, + "maximumBytesBilled"=> null, + "location"=> null, + "requestId"=> "myJobId", + "jobCreationMode"=> "JOB_CREATION_OPTIONAL", + "initialTimeoutMs"=> 10000, + "projectId"=> "my-awesome-project" ]) ->shouldBeCalledTimes(1) ->willReturn([ @@ -233,26 +239,32 @@ public function testRunQueryWithPositionalParameters() $snippet->addLocal('bigQuery', $this->client); $expectedQuery = 'SELECT commit FROM `bigquery-public-data.github_repos.commits` WHERE message = ? LIMIT 100'; $this->connection - ->insertJob([ - 'projectId' => self::PROJECT_ID, - 'jobReference' => ['projectId' => self::PROJECT_ID, 'jobId' => self::JOB_ID], - 'configuration' => [ - 'query' => [ - 'parameterMode' => 'positional', - 'useLegacySql' => false, - 'queryParameters' => [ - [ - 'parameterType' => [ - 'type' => 'STRING' - ], - 'parameterValue' => [ - 'value' => 'A commit message.' - ] - ] + ->query([ + "query"=> "SELECT commit FROM `bigquery-public-data.github_repos.commits` WHERE message = ? LIMIT 100", + "maxResults"=> null, + "defaultDataset"=> null, + "timeoutMs"=> 10000, + "useQueryCache"=> null, + "useLegacySql"=> false, + "queryParameters"=> [ + [ + "parameterType"=> [ + "type"=> "STRING" ], - 'query' => $expectedQuery + "parameterValue"=> [ + "value"=> "A commit message." + ] ] - ] + ], + "parameterMode"=> "positional", + "labels"=> null, + "createSession"=> null, + "maximumBytesBilled"=> null, + "location"=> null, + "requestId"=> "myJobId", + "jobCreationMode"=> "JOB_CREATION_OPTIONAL", + "initialTimeoutMs"=> 10000, + "projectId"=> "my-awesome-project" ]) ->shouldBeCalledTimes(1) ->willReturn([ diff --git a/BigQuery/tests/Unit/BigQueryClientTest.php b/BigQuery/tests/Unit/BigQueryClientTest.php index cb36f6f58c50..f7f7f5f5a0d8 100644 --- a/BigQuery/tests/Unit/BigQueryClientTest.php +++ b/BigQuery/tests/Unit/BigQueryClientTest.php @@ -157,6 +157,36 @@ public function testRunsQuery() $this->assertEquals(self::JOB_ID, $queryResults->identity()['jobId']); } + public function testRunQueryStateless() + { + $client = $this->getClient(); + $query = $client->query(self::QUERY_STRING); + + $this->connection->query(Argument::allOf( + Argument::withEntry('projectId', self::PROJECT_ID), + Argument::withEntry('query', self::QUERY_STRING), + Argument::withEntry('jobCreationMode', 'JOB_CREATION_OPTIONAL') + )) + ->willReturn([ + 'jobReference' => [ + 'jobId' => self::JOB_ID, + 'projectId' => self::PROJECT_ID, + 'location' => self::LOCATION + ], + 'jobComplete' => true, + 'schema' => ['fields' => []], + 'rows' => [] + ]) + ->shouldBeCalledTimes(1); + + $client->___setProperty('connection', $this->connection->reveal()); + $queryResults = $client->runQuery($query); + + $this->assertInstanceOf(QueryResults::class, $queryResults); + $this->assertEquals(self::JOB_ID, $queryResults->identity()['jobId']); + $this->assertTrue($queryResults->isComplete()); + } + public function testRunsQueryWithRetry() { $client = $this->getClient();