Skip to content
This repository was archived by the owner on Sep 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,22 +173,48 @@ public function cancelJob(string $queryJobId, array $requestBody = []): array
/**
* Get job results
*
* @return array<string, mixed>
* @return array{
* "columns": array<int, array{
* "name": string,
* "type": "text",
* }>,
* "data": array<array<int, string>>,
* "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);
}

/**
* Execute a workspace query and wait for results
*
* @param array{statements: string[], transactional?: bool} $requestBody
* @return array<string, mixed>
* @return array{
* queryJobId: string,
* status: string,
* statements: array<array<string, mixed>>,
* results: array{
* "columns": array<array{
* "name": string,
* "type": "text",
* }>,
* "data": array<array<int, string>>,
* "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,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mimo jiné jsem tu přidal nastavitelný čas na čekání na query

): array {
// Submit the query job
$response = $this->submitQueryJob($branchId, $workspaceId, $requestBody);

Expand All @@ -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 */
Expand All @@ -222,10 +249,13 @@ public function executeWorkspaceQuery(string $branchId, string $workspaceId, arr
}
}

/** @var array<array<string, mixed>> $statements */
$statements = $finalStatus['statements'] ?? [];

return [
'queryJobId' => $queryJobId,
'status' => $finalStatus['status'],
'statements' => $finalStatus['statements'] ?? [],
'statements' => $statements,
'results' => $results,
];
}
Expand Down
30 changes: 15 additions & 15 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
96 changes: 49 additions & 47 deletions tests/Functional/BasicQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]);
}
}
Loading