diff --git a/src/Client.php b/src/Client.php index 9e8faef..05d4ad9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -18,10 +18,11 @@ class Client { - private const DEFAULT_USER_AGENT = 'Keboola Query API PHP Client'; - private const DEFAULT_BACKOFF_RETRIES = 3; - private const GUZZLE_CONNECT_TIMEOUT_SECONDS = 10; - private const GUZZLE_TIMEOUT_SECONDS = 120; + private const string DEFAULT_USER_AGENT = 'Keboola Query API PHP Client'; + private const int DEFAULT_BACKOFF_RETRIES = 3; + private const int GUZZLE_CONNECT_TIMEOUT_SECONDS = 10; + private const int GUZZLE_TIMEOUT_SECONDS = 120; + private const int DEFAULT_MAX_WAIT_SECONDS = 30; private string $apiUrl; private string $tokenString; @@ -172,11 +173,20 @@ public function cancelJob(string $queryJobId, array $requestBody = []): array /** * Get job results * - * @return array + * @return array{ + * "columns": array, + * "data": array>, + * "status": string, + * "rowsAffected": int + * } */ public function getJobResults(string $queryJobId, string $statementId): array { $url = sprintf('/api/v1/queries/%s/%s/results', $queryJobId, $statementId); + /** @phpstan-ignore-next-line */ return $this->sendRequest('GET', $url); } @@ -184,10 +194,27 @@ public function getJobResults(string $queryJobId, string $statementId): array * Execute a workspace query and wait for results * * @param array{statements: string[], transactional?: bool} $requestBody - * @return array + * @return array{ + * queryJobId: string, + * status: string, + * statements: array>, + * results: array{ + * "columns": array, + * "data": array>, + * "status": string, + * "rowsAffected": int + * }[], + * } */ - public function executeWorkspaceQuery(string $branchId, string $workspaceId, array $requestBody): array - { + public function executeWorkspaceQuery( + string $branchId, + string $workspaceId, + array $requestBody, + int $maxWaitSeconds = self::DEFAULT_MAX_WAIT_SECONDS, + ): array { // Submit the query job $response = $this->submitQueryJob($branchId, $workspaceId, $requestBody); @@ -198,7 +225,7 @@ public function executeWorkspaceQuery(string $branchId, string $workspaceId, arr $queryJobId = $response['queryJobId']; // Wait for job completion - $finalStatus = $this->waitForJobCompletion($queryJobId); + $finalStatus = $this->waitForJobCompletion($queryJobId, $maxWaitSeconds); if (!isset($finalStatus['status']) || $finalStatus['status'] !== 'completed') { /** @var string $status */ @@ -222,10 +249,13 @@ public function executeWorkspaceQuery(string $branchId, string $workspaceId, arr } } + /** @var array> $statements */ + $statements = $finalStatus['statements'] ?? []; + return [ 'queryJobId' => $queryJobId, 'status' => $finalStatus['status'], - 'statements' => $finalStatus['statements'] ?? [], + 'statements' => $statements, 'results' => $results, ]; } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 9afce62..3917522 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -15,8 +15,8 @@ class ClientTest extends TestCase { public function testConstructorRequiresUrl(): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('url must be set'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('url must be set'); new Client([ 'token' => 'test-token', @@ -25,8 +25,8 @@ public function testConstructorRequiresUrl(): void public function testConstructorRequiresToken(): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('token must be set'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('token must be set'); new Client([ 'url' => 'https://test.keboola.com', @@ -46,7 +46,7 @@ public function testSubmitQueryJob(): void 'transactional' => true, ]); - $this->assertEquals(['queryJobId' => 'job-12345'], $result); + self::assertEquals(['queryJobId' => 'job-12345'], $result); } public function testGetJobStatus(): void @@ -63,8 +63,8 @@ public function testGetJobStatus(): void $result = $client->getJobStatus('job-12345'); - $this->assertEquals('job-12345', $result['queryJobId']); - $this->assertEquals('running', $result['status']); + self::assertEquals('job-12345', $result['queryJobId']); + self::assertEquals('running', $result['status']); } public function testCancelJob(): void @@ -77,7 +77,7 @@ public function testCancelJob(): void $result = $client->cancelJob('job-12345', ['reason' => 'User requested']); - $this->assertEquals(['queryJobId' => 'job-12345'], $result); + self::assertEquals(['queryJobId' => 'job-12345'], $result); } public function testGetJobResults(): void @@ -94,10 +94,10 @@ public function testGetJobResults(): void $result = $client->getJobResults('job-12345', 'stmt-67890'); - $this->assertEquals('completed', $result['status']); - $this->assertEquals(1, $result['rowsAffected']); + self::assertEquals('completed', $result['status']); + self::assertEquals(1, $result['rowsAffected']); assert(is_array($result['data'])); - $this->assertCount(1, $result['data']); + self::assertCount(1, $result['data']); } public function testHealthCheck(): void @@ -115,8 +115,8 @@ public function testHealthCheck(): void $result = $client->healthCheck(); - $this->assertEquals('query', $result['service']); - $this->assertEquals('ok', $result['status']); + self::assertEquals('query', $result['service']); + self::assertEquals('ok', $result['status']); } public function testHealthCheckWithInvalidToken(): void @@ -142,8 +142,8 @@ public function testHealthCheckWithInvalidToken(): void // Health check should succeed because no token is sent $result = $client->healthCheck(); - $this->assertEquals('query', $result['service']); - $this->assertEquals('ok', $result['status']); + self::assertEquals('query', $result['service']); + self::assertEquals('ok', $result['status']); } private function createClientWithMockHandler(MockHandler $mockHandler): Client diff --git a/tests/Functional/BasicQueryTest.php b/tests/Functional/BasicQueryTest.php index 88a2262..9e1fe27 100644 --- a/tests/Functional/BasicQueryTest.php +++ b/tests/Functional/BasicQueryTest.php @@ -18,45 +18,46 @@ public function testSubmitSimpleSelectQuery(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); - $this->assertNotEmpty($queryJobId); + self::assertNotEmpty($queryJobId); // Wait for job completion $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); - $this->assertEquals('completed', $finalStatus['status']); - $this->assertEquals($queryJobId, $finalStatus['queryJobId']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('completed', $finalStatus['status']); + self::assertEquals($queryJobId, $finalStatus['queryJobId']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('completed', $statement['status']); + self::assertEquals('completed', $statement['status']); // Get job results - $this->assertArrayHasKey('id', $statement); + self::assertArrayHasKey('id', $statement); $results = $this->queryClient->getJobResults($queryJobId, $statement['id']); - $this->assertArrayHasKey('status', $results); - $this->assertEquals('completed', $results['status']); + self::assertArrayHasKey('status', $results); + self::assertEquals('completed', $results['status']); // Verify we got a timestamp result - $this->assertArrayHasKey('data', $results); + self::assertArrayHasKey('data', $results); $data = $results['data']; assert(is_array($data)); - $this->assertCount(1, $data); + self::assertCount(1, $data); $row = $data[0]; assert(is_array($row)); - $this->assertCount(1, $row); + self::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]); + self::assertArrayHasKey(0, $row); + self::assertIsString($row[0]); + self::assertNotEmpty($row[0]); // Verify it's a valid timestamp (numeric string) - $this->assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); + self::assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); } public function testSubmitInformationSchemaQuery(): void @@ -74,44 +75,44 @@ public function testSubmitInformationSchemaQuery(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); - $this->assertNotEmpty($queryJobId); + self::assertNotEmpty($queryJobId); // Wait for job completion $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); - $this->assertEquals('completed', $finalStatus['status']); - $this->assertEquals($queryJobId, $finalStatus['queryJobId']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('completed', $finalStatus['status']); + self::assertEquals($queryJobId, $finalStatus['queryJobId']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('completed', $statement['status']); + self::assertEquals('completed', $statement['status']); // Get job results - $this->assertArrayHasKey('id', $statement); + self::assertArrayHasKey('id', $statement); $results = $this->queryClient->getJobResults($queryJobId, $statement['id']); - $this->assertArrayHasKey('status', $results); - $this->assertEquals('completed', $results['status']); + self::assertArrayHasKey('status', $results); + self::assertEquals('completed', $results['status']); // Verify we got a count result - $this->assertArrayHasKey('data', $results); + self::assertArrayHasKey('data', $results); $data = $results['data']; assert(is_array($data)); - $this->assertCount(1, $data); + self::assertCount(1, $data); $row = $data[0]; assert(is_array($row)); - $this->assertCount(1, $row); + self::assertCount(1, $row); // Query API returns indexed arrays, not associative arrays with column names assert(isset($row[0])); - $this->assertIsNumeric($row[0]); - $this->assertGreaterThanOrEqual(0, (int) $row[0]); + self::assertIsNumeric($row[0]); + self::assertGreaterThanOrEqual(0, (int) $row[0]); } public function testExecuteWorkspaceQuery(): void @@ -127,45 +128,46 @@ public function testExecuteWorkspaceQuery(): void ); // Verify the response structure - $this->assertArrayHasKey('queryJobId', $response); - $this->assertArrayHasKey('status', $response); - $this->assertArrayHasKey('statements', $response); - $this->assertArrayHasKey('results', $response); + self::assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('status', $response); + self::assertArrayHasKey('statements', $response); + self::assertArrayHasKey('results', $response); // Verify job completed successfully - $this->assertEquals('completed', $response['status']); - $this->assertNotEmpty($response['queryJobId']); + self::assertEquals('completed', $response['status']); + self::assertNotEmpty($response['queryJobId']); // Verify statements $statements = $response['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('completed', $statement['status']); + self::assertEquals('completed', $statement['status']); // Verify results $results = $response['results']; assert(is_array($results)); - $this->assertCount(1, $results); + self::assertCount(1, $results); $result = $results[0]; assert(is_array($result)); - $this->assertEquals('completed', $result['status']); + self::assertEquals('completed', $result['status']); // Verify we got timestamp data - $this->assertArrayHasKey('data', $result); + self::assertArrayHasKey('data', $result); $data = $result['data']; assert(is_array($data)); - $this->assertCount(1, $data); + self::assertCount(1, $data); $row = $data[0]; assert(is_array($row)); - $this->assertCount(1, $row); + self::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]); + self::assertArrayHasKey(0, $row); + self::assertIsString($row[0]); + self::assertNotEmpty($row[0]); // Verify it's a valid timestamp (numeric string) - $this->assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); + self::assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); } } diff --git a/tests/Functional/QueryServiceFunctionalTest.php b/tests/Functional/QueryServiceFunctionalTest.php index 9405f53..6f19594 100644 --- a/tests/Functional/QueryServiceFunctionalTest.php +++ b/tests/Functional/QueryServiceFunctionalTest.php @@ -13,13 +13,13 @@ public function testHealthCheck(): void { $result = $this->queryClient->healthCheck(); - $this->assertArrayHasKey('service', $result); - $this->assertArrayHasKey('status', $result); - $this->assertArrayHasKey('timestamp', $result); - $this->assertArrayHasKey('version', $result); + self::assertArrayHasKey('service', $result); + self::assertArrayHasKey('status', $result); + self::assertArrayHasKey('timestamp', $result); + self::assertArrayHasKey('version', $result); - $this->assertEquals('query', $result['service']); - $this->assertEquals('ok', $result['status']); + self::assertEquals('query', $result['service']); + self::assertEquals('ok', $result['status']); } public function testSubmitAndGetSimpleQuery(): void @@ -37,41 +37,41 @@ public function testSubmitAndGetSimpleQuery(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); - $this->assertNotEmpty($queryJobId); + self::assertNotEmpty($queryJobId); // Wait for job completion $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); - $this->assertEquals('completed', $finalStatus['status']); - $this->assertEquals($queryJobId, $finalStatus['queryJobId']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('completed', $finalStatus['status']); + self::assertEquals($queryJobId, $finalStatus['queryJobId']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('completed', $statement['status']); + self::assertEquals('completed', $statement['status']); // Get job results - $this->assertArrayHasKey('id', $statement); + self::assertArrayHasKey('id', $statement); $results = $this->queryClient->getJobResults($queryJobId, $statement['id']); - $this->assertArrayHasKey('data', $results); - $this->assertArrayHasKey('status', $results); - $this->assertEquals('completed', $results['status']); + self::assertArrayHasKey('data', $results); + self::assertArrayHasKey('status', $results); + self::assertEquals('completed', $results['status']); // Verify the result contains our count - $this->assertArrayHasKey('data', $results); + self::assertArrayHasKey('data', $results); $data = $results['data']; assert(is_array($data)); - $this->assertCount(1, $data); + self::assertCount(1, $data); $row = $data[0]; assert(is_array($row)); - $this->assertEquals(3, $row[0]); // We inserted 3 rows + self::assertEquals(3, $row[0]); // We inserted 3 rows } public function testSubmitTransactionalQuery(): void @@ -92,37 +92,37 @@ public function testSubmitTransactionalQuery(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); // Wait for completion $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); - $this->assertEquals('completed', $finalStatus['status']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('completed', $finalStatus['status']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(2, $statements); + self::assertCount(2, $statements); // Check INSERT statement $insertStatement = $statements[0]; assert(is_array($insertStatement)); - $this->assertEquals('completed', $insertStatement['status']); + self::assertEquals('completed', $insertStatement['status']); // Check SELECT statement and its results $selectStatement = $statements[1]; assert(is_array($selectStatement)); - $this->assertEquals('completed', $selectStatement['status']); + self::assertEquals('completed', $selectStatement['status']); - $this->assertArrayHasKey('id', $selectStatement); + self::assertArrayHasKey('id', $selectStatement); $results = $this->queryClient->getJobResults($queryJobId, $selectStatement['id']); - $this->assertArrayHasKey('data', $results); + self::assertArrayHasKey('data', $results); $data = $results['data']; assert(is_array($data)); $row = $data[0]; assert(is_array($row)); - $this->assertEquals(4, $row[0]); // Should be 4 rows now + self::assertEquals(4, $row[0]); // Should be 4 rows now } public function testCancelQueryJob(): void @@ -148,32 +148,32 @@ public function testCancelQueryJob(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); - $this->assertNotEmpty($queryJobId); + self::assertNotEmpty($queryJobId); // Cancel the job $cancelResponse = $this->queryClient->cancelJob($queryJobId, [ 'reason' => 'Test cancellation', ]); - $this->assertEquals($queryJobId, $cancelResponse['queryJobId']); + self::assertEquals($queryJobId, $cancelResponse['queryJobId']); // Wait for final status $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId, 15); // Job should be canceled - $this->assertEquals('canceled', $finalStatus['status']); - $this->assertArrayHasKey('cancellationReason', $finalStatus); - $this->assertEquals('Test cancellation', $finalStatus['cancellationReason']); - $this->assertArrayHasKey('canceledAt', $finalStatus); + self::assertEquals('canceled', $finalStatus['status']); + self::assertArrayHasKey('cancellationReason', $finalStatus); + self::assertEquals('Test cancellation', $finalStatus['cancellationReason']); + self::assertArrayHasKey('canceledAt', $finalStatus); // Verify job has statements but don't assert on their status - $this->assertArrayHasKey('statements', $finalStatus); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); } public function testQueryJobWithInvalidSQL(): void @@ -188,7 +188,7 @@ public function testQueryJobWithInvalidSQL(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); @@ -196,17 +196,17 @@ public function testQueryJobWithInvalidSQL(): void $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); // Job should fail due to invalid SQL - $this->assertEquals('failed', $finalStatus['status']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('failed', $finalStatus['status']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('failed', $statement['status']); + self::assertEquals('failed', $statement['status']); assert(is_string($statement['query'])); - $this->assertEquals('SELECT * FROM non_existent_table_12345', $statement['query']); + self::assertEquals('SELECT * FROM non_existent_table_12345', $statement['query']); } public function testQueryJobWithEmptyStatements(): void @@ -235,7 +235,7 @@ public function testQueryJobWithInvalidBranch(): void ], ); - $this->assertArrayHasKey('queryJobId', $response); + self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; assert(is_string($queryJobId)); @@ -243,19 +243,19 @@ public function testQueryJobWithInvalidBranch(): void $finalStatus = $this->queryClient->waitForJobCompletion($queryJobId); // Query Service accepts invalid branch IDs and executes successfully - $this->assertEquals('completed', $finalStatus['status']); - $this->assertArrayHasKey('statements', $finalStatus); + self::assertEquals('completed', $finalStatus['status']); + self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; assert(is_array($statements)); - $this->assertCount(1, $statements); + self::assertCount(1, $statements); $statement = $statements[0]; assert(is_array($statement)); - $this->assertEquals('completed', $statement['status']); + self::assertEquals('completed', $statement['status']); assert(is_string($statement['query'])); - $this->assertEquals('SELECT 1', $statement['query']); + self::assertEquals('SELECT 1', $statement['query']); assert(is_int($statement['rowsAffected'])); - $this->assertEquals(0, $statement['rowsAffected']); + self::assertEquals(0, $statement['rowsAffected']); } public function testQueryJobWithInvalidWorkspace(): void diff --git a/tests/Phpunit/ResultHelperTest.php b/tests/Phpunit/ResultHelperTest.php index 5c800ff..96b1c51 100644 --- a/tests/Phpunit/ResultHelperTest.php +++ b/tests/Phpunit/ResultHelperTest.php @@ -40,8 +40,8 @@ public function testMapColumnNamesIntoData(): void $actual = ResultHelper::mapColumnNamesIntoData($input); // Columns should be preserved unchanged - $this->assertSame($expected['columns'], $actual['columns']); + self::assertSame($expected['columns'], $actual['columns']); // Data rows should be mapped by column names - $this->assertSame($expected['data'], $actual['data']); + self::assertSame($expected['data'], $actual['data']); } }