diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 9cae8b2..9574568 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -8,17 +8,17 @@ on: jobs: build: runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v2 - + - name: Build development container run: docker compose build dev - + - name: Install dependencies run: docker compose run --rm dev composer install - + - name: Run tests and CI checks run: | docker compose run --rm \ diff --git a/.gitignore b/.gitignore index 8773461..34144c0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Environment files (contains sensitive information) .env +.env.local # PHPUnit .phpunit.result.cache diff --git a/Dockerfile b/Dockerfile index 7335b4f..458245f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN apt-get update -q \ && apt-get install git unzip \ -y --no-install-recommends +RUN git config --global --add safe.directory /code RUN echo "memory_limit = -1" >> /usr/local/etc/php/php.ini RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer diff --git a/phpcs.xml b/phpcs.xml index 513fae5..74dfbed 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,9 +1,6 @@ - - The Keboola coding standard. - - src - tests - - + + + + */src/Kernel.php diff --git a/src/Client.php b/src/Client.php index 6fa98f0..1e5a7a9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -228,11 +228,9 @@ public function executeWorkspaceQuery( $finalStatus = $this->waitForJobCompletion($queryJobId, $maxWaitSeconds); if (!isset($finalStatus['status']) || $finalStatus['status'] !== 'completed') { - /** @var string $status */ - $status = $finalStatus['status'] ?? 'unknown'; throw new ClientException( - sprintf('Query job failed with status: %s', $status), - $status === 'failed' ? 500 : 0, + sprintf('Query job failed with error: %s', ResultHelper::extractAllStatementErrors($finalStatus)), + 400, ); } diff --git a/src/ResultHelper.php b/src/ResultHelper.php index 9014dfb..e1844db 100644 --- a/src/ResultHelper.php +++ b/src/ResultHelper.php @@ -41,4 +41,28 @@ public static function mapColumnNamesIntoData(array $responseData): array $responseData['data'] = $transformedData; return $responseData; } + + /** + * @param array $responseData + */ + public static function extractAllStatementErrors(array $responseData): string + { + $errors = []; + if (isset($responseData['statements']) && is_array($responseData['statements'])) { + foreach ($responseData['statements'] as $statement) { + if (is_array($statement) && isset($statement['error']) && is_string($statement['error'])) { + $err = trim($statement['error']); + if ($err !== '') { + $errors[] = $err; + } + } + } + } + + if (!$errors) { + return 'Unknown error'; + } + + return implode("\n", $errors); + } } diff --git a/tests/Functional/BasicQueryTest.php b/tests/Functional/BasicQueryTest.php index 9e1fe27..c24442c 100644 --- a/tests/Functional/BasicQueryTest.php +++ b/tests/Functional/BasicQueryTest.php @@ -4,6 +4,8 @@ namespace Keboola\QueryApi\Tests\Functional; +use Keboola\QueryApi\ClientException; + class BasicQueryTest extends BaseFunctionalTestCase { public function testSubmitSimpleSelectQuery(): void @@ -20,7 +22,7 @@ public function testSubmitSimpleSelectQuery(): void self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; - assert(is_string($queryJobId)); + self::assertIsString($queryJobId); self::assertNotEmpty($queryJobId); // Wait for job completion @@ -30,11 +32,11 @@ public function testSubmitSimpleSelectQuery(): void self::assertEquals($queryJobId, $finalStatus['queryJobId']); self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; - assert(is_array($statements)); + self::assertIsArray($statements); self::assertCount(1, $statements); $statement = $statements[0]; - assert(is_array($statement)); + self::assertIsArray($statement); self::assertEquals('completed', $statement['status']); // Get job results @@ -47,10 +49,10 @@ public function testSubmitSimpleSelectQuery(): void // Verify we got a timestamp result self::assertArrayHasKey('data', $results); $data = $results['data']; - assert(is_array($data)); + self::assertIsArray($data); self::assertCount(1, $data); $row = $data[0]; - assert(is_array($row)); + self::assertIsArray($row); self::assertCount(1, $row); // Query API returns indexed arrays, not associative arrays with column names self::assertArrayHasKey(0, $row); @@ -77,7 +79,7 @@ public function testSubmitInformationSchemaQuery(): void self::assertArrayHasKey('queryJobId', $response); $queryJobId = $response['queryJobId']; - assert(is_string($queryJobId)); + self::assertIsString($queryJobId); self::assertNotEmpty($queryJobId); // Wait for job completion @@ -87,11 +89,11 @@ public function testSubmitInformationSchemaQuery(): void self::assertEquals($queryJobId, $finalStatus['queryJobId']); self::assertArrayHasKey('statements', $finalStatus); $statements = $finalStatus['statements']; - assert(is_array($statements)); + self::assertIsArray($statements); self::assertCount(1, $statements); $statement = $statements[0]; - assert(is_array($statement)); + self::assertIsArray($statement); self::assertEquals('completed', $statement['status']); // Get job results @@ -104,13 +106,13 @@ public function testSubmitInformationSchemaQuery(): void // Verify we got a count result self::assertArrayHasKey('data', $results); $data = $results['data']; - assert(is_array($data)); + self::assertIsArray($data); self::assertCount(1, $data); $row = $data[0]; - assert(is_array($row)); + self::assertIsArray($row); self::assertCount(1, $row); // Query API returns indexed arrays, not associative arrays with column names - assert(isset($row[0])); + self::assertArrayHasKey(0, $row); self::assertIsNumeric($row[0]); self::assertGreaterThanOrEqual(0, (int) $row[0]); } @@ -139,29 +141,29 @@ public function testExecuteWorkspaceQuery(): void // Verify statements $statements = $response['statements']; - assert(is_array($statements)); + self::assertIsArray($statements); self::assertCount(1, $statements); $statement = $statements[0]; - assert(is_array($statement)); + self::assertIsArray($statement); self::assertEquals('completed', $statement['status']); // Verify results $results = $response['results']; - assert(is_array($results)); + self::assertIsArray($results); self::assertCount(1, $results); $result = $results[0]; - assert(is_array($result)); + self::assertIsArray($result); self::assertEquals('completed', $result['status']); // Verify we got timestamp data self::assertArrayHasKey('data', $result); $data = $result['data']; - assert(is_array($data)); + self::assertIsArray($data); self::assertCount(1, $data); $row = $data[0]; - assert(is_array($row)); + self::assertIsArray($row); self::assertCount(1, $row); // Query API returns indexed arrays, not associative arrays with column names self::assertArrayHasKey(0, $row); @@ -170,4 +172,19 @@ public function testExecuteWorkspaceQuery(): void // Verify it's a valid timestamp (numeric string) self::assertMatchesRegularExpression('/^\d+\.\d+$/', $row[0]); } + + public function testExecuteInvalidWorkspaceQuery(): void + { + self::expectException(ClientException::class); + self::expectExceptionMessage('\'COOTIES\' does not exist or not authorized'); + self::expectExceptionCode(400); + $this->queryClient->executeWorkspaceQuery( + $this->getTestBranchId(), + $this->getTestWorkspaceId(), + [ + 'statements' => ['SELECT 1', 'SELECT * FROM Cooties', 'SELECT 2'], + 'transactional' => false, + ], + ); + } } diff --git a/tests/Phpunit/ResultHelperTest.php b/tests/Phpunit/ResultHelperTest.php index 96b1c51..5a22301 100644 --- a/tests/Phpunit/ResultHelperTest.php +++ b/tests/Phpunit/ResultHelperTest.php @@ -44,4 +44,48 @@ public function testMapColumnNamesIntoData(): void // Data rows should be mapped by column names self::assertSame($expected['data'], $actual['data']); } + + public function testExtractAllStatementErrorsSingle(): void + { + $responseData = [ + 'queryJobId' => '74001bf0-c79c-49f7-bad9-ef96db5ff28e', + 'status' => 'failed', + 'statements' => [ + [ + 'id' => 'b0065ce1-00d5-4723-8897-0c5a902ae446', + 'query' => 'SELECT 1', + 'status' => 'completed', + ], + [ + 'id' => '4feb6663-c98c-401d-a875-5bb72438e2cc', + 'query' => 'SELECT * FROM Cooties', + 'status' => 'failed', + 'error' => 'COOTIES does not exist or not authorized', + ], + [ + 'id' => '4feb6663-c98c-401d-a875-5bb72438e2cc', + 'query' => 'a spacey query', + 'status' => 'failed', + 'error' => ' there is also a lot of space ', + ], + [ + 'id' => 'a57bc659-c9f7-45d4-a011-d6f10cc0e757', + 'query' => 'SELECT 2', + 'status' => 'notExecuted', + ], + ], + ]; + + $actual = ResultHelper::extractAllStatementErrors($responseData); + self::assertEquals( + "COOTIES does not exist or not authorized\nthere is also a lot of space", + $actual, + ); + } + + public function testExtractAllStatementErrorsInvalidArray(): void + { + $actual = ResultHelper::extractAllStatementErrors([]); + self::assertSame('Unknown error', $actual); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ad8e8b3..f3f514c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -6,11 +6,6 @@ require_once __DIR__ . '/../vendor/autoload.php'; -// Load environment variables from .env file if it exists (for local development) -if (class_exists('Symfony\Component\Dotenv\Dotenv')) { - $envFile = __DIR__ . '/../.env'; - if (file_exists($envFile)) { - $dotenv = new Dotenv(); - $dotenv->load($envFile); - } -} +$dotEnv = new Dotenv(); +$dotEnv->usePutenv(); +$dotEnv->bootEnv(dirname(__DIR__) . '/.env', 'dev', []);