From c0d1bc9c9a24157aa3458422673fad3a876bf377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Wed, 20 Aug 2025 14:06:51 +0200 Subject: [PATCH 1/4] Add executeWorkspaceQuery method with automatic job completion waiting --- src/Client.php | 75 +++++++++++++++++++++++++++++ tests/Functional/BasicQueryTest.php | 55 +++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/src/Client.php b/src/Client.php index b3b1c25..968697b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -180,6 +180,56 @@ public function getJobResults(string $queryJobId, string $statementId): array return $this->sendRequest('GET', $url); } + /** + * Execute a workspace query and wait for results + * + * @param array{statements: string[], transactional?: bool} $requestBody + * @return array + */ + public function executeWorkspaceQuery(string $branchId, string $workspaceId, array $requestBody): array + { + // Submit the query job + $response = $this->submitQueryJob($branchId, $workspaceId, $requestBody); + + if (!isset($response['queryJobId']) || !is_string($response['queryJobId'])) { + throw new ClientException('Invalid response from submitQueryJob: missing queryJobId'); + } + + $queryJobId = $response['queryJobId']; + + // Wait for job completion + $finalStatus = $this->waitForJobCompletion($queryJobId); + + if (!isset($finalStatus['status']) || $finalStatus['status'] !== 'completed') { + $status = $finalStatus['status'] ?? 'unknown'; + assert(is_string($status)); + throw new ClientException( + sprintf('Query job failed with status: %s', $status), + $status === 'failed' ? 500 : 0, + ); + } + + // Get results for all completed statements + $results = []; + if (isset($finalStatus['statements']) && is_array($finalStatus['statements'])) { + foreach ($finalStatus['statements'] as $statement) { + if (is_array($statement) && isset($statement['id']) && isset($statement['status'])) { + if ($statement['status'] === 'completed') { + $statementResults = $this->getJobResults($queryJobId, $statement['id']); + $results[] = $statementResults; + } + } + } + } + + return [ + 'queryJobId' => $queryJobId, + 'status' => $finalStatus['status'], + 'statements' => $finalStatus['statements'] ?? [], + 'results' => $results, + ]; + } + /** * Health check * @@ -269,4 +319,29 @@ private function handleGuzzleException(GuzzleException $e): void throw new ClientException('Query Service API request failed: ' . $e->getMessage(), 0, $e); } + + /** + * Wait for job completion with timeout + * + * @param int $maxWaitSeconds Maximum time to wait in seconds + * @return array + */ + private function waitForJobCompletion(string $queryJobId, int $maxWaitSeconds = 30): array + { + $startTime = time(); + + while (time() - $startTime < $maxWaitSeconds) { + $status = $this->getJobStatus($queryJobId); + + if (in_array($status['status'], ['completed', 'failed', 'canceled'], true)) { + return $status; + } + + sleep(1); + } + + throw new ClientException( + sprintf('Job %s did not complete within %d seconds', $queryJobId, $maxWaitSeconds), + ); + } } diff --git a/tests/Functional/BasicQueryTest.php b/tests/Functional/BasicQueryTest.php index c464a29..253703a 100644 --- a/tests/Functional/BasicQueryTest.php +++ b/tests/Functional/BasicQueryTest.php @@ -113,4 +113,59 @@ public function testSubmitInformationSchemaQuery(): void $this->assertIsNumeric($row[0]); $this->assertGreaterThanOrEqual(0, (int) $row[0]); } + + public function testExecuteWorkspaceQuery(): void + { + // Test the new executeWorkspaceQuery method with a simple query + $response = $this->queryClient->executeWorkspaceQuery( + $this->getTestBranchId(), + $this->getTestWorkspaceId(), + [ + 'statements' => ['SELECT CURRENT_TIMESTAMP() AS "current_time"'], + 'transactional' => false, + ], + ); + + // Verify the response structure + $this->assertArrayHasKey('queryJobId', $response); + $this->assertArrayHasKey('status', $response); + $this->assertArrayHasKey('statements', $response); + $this->assertArrayHasKey('results', $response); + + // Verify job completed successfully + $this->assertEquals('completed', $response['status']); + $this->assertNotEmpty($response['queryJobId']); + + // Verify statements + $statements = $response['statements']; + assert(is_array($statements)); + $this->assertCount(1, $statements); + + $statement = $statements[0]; + assert(is_array($statement)); + $this->assertEquals('completed', $statement['status']); + + // Verify results + $results = $response['results']; + assert(is_array($results)); + $this->assertCount(1, $results); + + $result = $results[0]; + assert(is_array($result)); + $this->assertEquals('completed', $result['status']); + + // Verify we got timestamp data + $this->assertArrayHasKey('data', $result); + $data = $result['data']; + assert(is_array($data)); + $this->assertCount(1, $data); + $row = $data[0]; + assert(is_array($row)); + $this->assertCount(1, $row); + // Query API returns indexed arrays, not associative arrays with column names + assert(isset($row[0]) && is_string($row[0])); + $this->assertNotEmpty($row[0]); + // Verify it's a valid timestamp (numeric string) + $this->assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); + } } From 854550f74f77d0b41565ecee757ccf638f270a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimi=CC=81r=20Kris=CC=8Cka?= Date: Wed, 20 Aug 2025 23:52:54 +0200 Subject: [PATCH 2/4] Make wait for completion public --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 968697b..eb20292 100644 --- a/src/Client.php +++ b/src/Client.php @@ -326,7 +326,7 @@ private function handleGuzzleException(GuzzleException $e): void * @param int $maxWaitSeconds Maximum time to wait in seconds * @return array */ - private function waitForJobCompletion(string $queryJobId, int $maxWaitSeconds = 30): array + public function waitForJobCompletion(string $queryJobId, int $maxWaitSeconds = 30): array { $startTime = time(); From d1f3bc066e29685761e0291cdd7f5f70273aa061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimi=CC=81r=20Kris=CC=8Cka?= Date: Wed, 20 Aug 2025 23:54:25 +0200 Subject: [PATCH 3/4] Use waitForJobCompletion where possible; Delete old method --- tests/Functional/BaseFunctionalTestCase.php | 24 +------------------ tests/Functional/BasicQueryTest.php | 4 ++-- .../Functional/QueryServiceFunctionalTest.php | 10 ++++---- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/tests/Functional/BaseFunctionalTestCase.php b/tests/Functional/BaseFunctionalTestCase.php index a75ef92..dc48e94 100644 --- a/tests/Functional/BaseFunctionalTestCase.php +++ b/tests/Functional/BaseFunctionalTestCase.php @@ -158,30 +158,8 @@ protected function createTestTable(?string $tableName = null): string // Wait for completion assert(is_string($response['queryJobId'])); - $this->waitForJobCompletion($response['queryJobId']); + $this->queryClient->waitForJobCompletion($response['queryJobId']); return $tableName; } - - /** - * @return array - */ - protected function waitForJobCompletion(string $queryJobId, int $maxWaitSeconds = 30): array - { - $startTime = time(); - $attempt = 1; - - while (time() - $startTime < $maxWaitSeconds) { - $status = $this->queryClient->getJobStatus($queryJobId); - if (in_array($status['status'], ['completed', 'failed', 'canceled'], true)) { - return $status; - } - sleep(1); - $attempt++; - } - - throw new RuntimeException( - sprintf('Job %s did not complete within %d seconds', $queryJobId, $maxWaitSeconds), - ); - } } diff --git a/tests/Functional/BasicQueryTest.php b/tests/Functional/BasicQueryTest.php index 253703a..88a2262 100644 --- a/tests/Functional/BasicQueryTest.php +++ b/tests/Functional/BasicQueryTest.php @@ -24,7 +24,7 @@ public function testSubmitSimpleSelectQuery(): void $this->assertNotEmpty($queryJobId); // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); $this->assertEquals('completed', $finalStatus['status']); $this->assertEquals($queryJobId, $finalStatus['queryJobId']); @@ -80,7 +80,7 @@ public function testSubmitInformationSchemaQuery(): void $this->assertNotEmpty($queryJobId); // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); $this->assertEquals('completed', $finalStatus['status']); $this->assertEquals($queryJobId, $finalStatus['queryJobId']); diff --git a/tests/Functional/QueryServiceFunctionalTest.php b/tests/Functional/QueryServiceFunctionalTest.php index 5957549..365e461 100644 --- a/tests/Functional/QueryServiceFunctionalTest.php +++ b/tests/Functional/QueryServiceFunctionalTest.php @@ -43,7 +43,7 @@ public function testSubmitAndGetSimpleQuery(): void $this->assertNotEmpty($queryJobId); // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); $this->assertEquals('completed', $finalStatus['status']); $this->assertEquals($queryJobId, $finalStatus['queryJobId']); @@ -97,7 +97,7 @@ public function testSubmitTransactionalQuery(): void assert(is_string($queryJobId)); // Wait for completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); $this->assertEquals('completed', $finalStatus['status']); $this->assertArrayHasKey('statements', $finalStatus); @@ -161,7 +161,7 @@ public function testCancelQueryJob(): void $this->assertEquals($queryJobId, $cancelResponse['queryJobId']); // Wait for final status - $finalStatus = $this->waitForJobCompletion($queryJobId, 15); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId, 15); // Job should be canceled $this->assertEquals('canceled', $finalStatus['status']); @@ -193,7 +193,7 @@ public function testQueryJobWithInvalidSQL(): void assert(is_string($queryJobId)); // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); // Job should fail due to invalid SQL $this->assertEquals('failed', $finalStatus['status']); @@ -241,7 +241,7 @@ public function testQueryJobWithInvalidBranch(): void assert(is_string($queryJobId)); // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); // Query Service accepts invalid branch IDs and executes successfully $this->assertEquals('completed', $finalStatus['status']); From 6a9df58719febd03dde8bb9657cb9f97aaee7af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Jodas?= Date: Thu, 21 Aug 2025 10:03:00 +0200 Subject: [PATCH 4/4] Remove assert and add type annotation for phpstan compatibility --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index eb20292..9e8faef 100644 --- a/src/Client.php +++ b/src/Client.php @@ -201,8 +201,8 @@ public function executeWorkspaceQuery(string $branchId, string $workspaceId, arr $finalStatus = $this->waitForJobCompletion($queryJobId); if (!isset($finalStatus['status']) || $finalStatus['status'] !== 'completed') { + /** @var string $status */ $status = $finalStatus['status'] ?? 'unknown'; - assert(is_string($status)); throw new ClientException( sprintf('Query job failed with status: %s', $status), $status === 'failed' ? 500 : 0,