Skip to content

Commit

Permalink
Merge 3.1.x into 4.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
morozov committed Apr 19, 2021
2 parents 6d84afd + 5ba62e7 commit fcc469d
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/workflows/static-analysis.yml
Expand Up @@ -52,3 +52,4 @@ jobs:
uses: docker://vimeo/psalm-github-actions:4.6.4
with:
composer_require_dev: true
args: --shepherd
25 changes: 14 additions & 11 deletions README.md
@@ -1,10 +1,11 @@
# Doctrine DBAL

| [4.0-dev][4.0] | [3.0][3.0] | [2.12][2.12] |
| [4.0-dev][4.0] | [3.0][3.0] | [2.13][2.13] |
|:----------------:|:----------:|:----------:|
| [![GitHub Actions][GA 4.0 image]][GA 4.0] | [![GitHub Actions][GA 3.0 image]][GA 3.0] | [![GitHub Actions][GA 2.12 image]][GA 2.12] |
| [![AppVeyor][AppVeyor 4.0 image]][AppVeyor 4.0] | [![AppVeyor][AppVeyor 3.0 image]][AppVeyor 3.0] | [![AppVeyor][AppVeyor 2.12 image]][AppVeyor 2.12] |
| [![Code Coverage][Coverage image]][CodeCov 4.0] | [![Code Coverage][Coverage 3.0 image]][CodeCov 3.0] | [![Code Coverage][Coverage 2.12 image]][CodeCov 2.12] |
| [![GitHub Actions][GA 4.0 image]][GA 4.0] | [![GitHub Actions][GA 3.0 image]][GA 3.0] | [![GitHub Actions][GA 2.13 image]][GA 2.13] |
| [![AppVeyor][AppVeyor 4.0 image]][AppVeyor 4.0] | [![AppVeyor][AppVeyor 3.0 image]][AppVeyor 3.0] | [![AppVeyor][AppVeyor 2.13 image]][AppVeyor 2.13] |
| [![Code Coverage][Coverage image]][CodeCov 4.0] | [![Code Coverage][Coverage 3.0 image]][CodeCov 3.0] | [![Code Coverage][Coverage 2.13 image]][CodeCov 2.13] |
| N/A | [![Code Coverage][TypeCov 3.1 image]][TypeCov 3.1] | N/A |

Powerful database abstraction layer with many features for database schema introspection, schema management and PDO abstraction.

Expand All @@ -30,10 +31,12 @@ Powerful database abstraction layer with many features for database schema intro
[GA 3.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.0.x
[GA 3.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.0.x

[Coverage 2.12 image]: https://codecov.io/gh/doctrine/dbal/branch/2.12.x/graph/badge.svg
[2.12]: https://github.com/doctrine/dbal/tree/2.12.x
[CodeCov 2.12]: https://codecov.io/gh/doctrine/dbal/branch/2.12.x
[AppVeyor 2.12]: https://ci.appveyor.com/project/doctrine/dbal/branch/2.12.x
[AppVeyor 2.12 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/2.12.x?svg=true
[GA 2.12]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A2.12.x
[GA 2.12 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=2.12.x
[Coverage 2.13 image]: https://codecov.io/gh/doctrine/dbal/branch/2.13.x/graph/badge.svg
[2.13]: https://github.com/doctrine/dbal/tree/2.13.x
[CodeCov 2.13]: https://codecov.io/gh/doctrine/dbal/branch/2.13.x
[AppVeyor 2.13]: https://ci.appveyor.com/project/doctrine/dbal/branch/2.13.x
[AppVeyor 2.13 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/2.13.x?svg=true
[GA 2.13]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A2.13.x
[GA 2.13 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=2.13.x
[TypeCov 3.1]: https://shepherd.dev/github/doctrine/dbal
[TypeCov 3.1 image]: https://shepherd.dev/github/doctrine/dbal/coverage.svg
5 changes: 5 additions & 0 deletions psalm.xml.dist
Expand Up @@ -54,6 +54,11 @@
See https://github.com/doctrine/dbal/pull/4317
-->
<file name="tests/Functional/LegacyAPITest.php"/>
<!--
These suppressions should be removed in 4.0.0
-->
<referencedMethod name="Doctrine\DBAL\Query\QueryBuilder::execute"/>
<referencedMethod name="Doctrine\DBAL\Statement::execute"/>
</errorLevel>
</DeprecatedMethod>
<DocblockTypeContradiction>
Expand Down
16 changes: 12 additions & 4 deletions src/Driver/OCI8/Result.php
Expand Up @@ -4,10 +4,13 @@

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\Result as ResultInterface;

use function oci_cancel;
use function oci_error;
use function oci_fetch_all;
use function oci_fetch_array;
use function oci_num_fields;
Expand Down Expand Up @@ -112,13 +115,18 @@ public function free(): void

/**
* @return mixed|false
*
* @throws Exception
*/
private function fetch(int $mode)
{
return oci_fetch_array(
$this->statement,
$mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS
);
$result = oci_fetch_array($this->statement, $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS);

if ($result === false && oci_error($this->statement) !== false) {
throw Error::new($this->statement);
}

return $result;
}

/**
Expand Down
41 changes: 41 additions & 0 deletions src/Statement.php
Expand Up @@ -8,6 +8,7 @@
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;

use function is_string;

Expand Down Expand Up @@ -156,10 +157,18 @@ public function bindParam($param, &$variable, int $type = ParameterType::STRING,
/**
* {@inheritDoc}
*
* @deprecated Statement::execute() is deprecated, use Statement::executeQuery() or executeStatement() instead
*
* @throws Exception
*/
public function execute(?array $params = null): Result
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/4580',
'Statement::execute() is deprecated, use Statement::executeQuery() or Statement::executeStatement() instead'
);

if ($params !== null) {
$this->params = $params;
}
Expand All @@ -179,6 +188,38 @@ public function execute(?array $params = null): Result
}
}

/**
* Executes the statement with the currently bound parameters and return result.
*
* @param mixed[] $params
*
* @throws Exception
*/
public function executeQuery(array $params = []): Result
{
if ($params === []) {
$params = null; // Workaround as long execute() exists and used internally.
}

return $this->execute($params);
}

/**
* Executes the statement with the currently bound parameters and return affected rows.
*
* @param mixed[] $params
*
* @throws Exception
*/
public function executeStatement(array $params = []): int
{
if ($params === []) {
$params = null; // Workaround as long execute() exists and used internally.
}

return $this->execute($params)->rowCount();
}

/**
* Gets the wrapped driver statement.
*/
Expand Down
170 changes: 170 additions & 0 deletions tests/Functional/Driver/OCI8/ResultTest.php
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Driver\OCI8;

use Doctrine\DBAL\Driver\OCI8\Driver;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Generator;

use function ini_get;
use function sprintf;

use const E_ALL;
use const E_WARNING;

/**
* @requires extension oci8
*/
class ResultTest extends FunctionalTestCase
{
/**
* Database connection parameters for functional test case
*
* @var array<string,mixed>
*/
private $connectionParams;

protected function setUp(): void
{
parent::setUp();
$this->connectionParams = TestUtil::getConnectionParams();

if ($this->connection->getDriver() instanceof Driver) {
return;
}

self::markTestSkipped('oci8 only test.');
}

protected function tearDown(): void
{
$this->connection->executeQuery('DROP FUNCTION test_oracle_fetch_failure');
$this->connection->executeQuery('DROP TYPE return_numbers');

parent::tearDown();
}

/**
* This test will recreate the case where a data set that is larger than the
* oci8 default prefetch is invalidated on the database after a fetch has begun,
* but before the fetch has completed.
*
* Note that this test requires 2 separate user connections so that the
* pipelined function can be changed mid fetch.
*
* @dataProvider dataProviderForTestTruncatedFetch
*/
public function testTruncatedFetch(
bool $invalidateDataMidFetch
): void {
if ($invalidateDataMidFetch) {
// prevent the PHPUnit error handler from handling the warnings that oci_*() functions may trigger
$this->iniSet('error_reporting', (string) (E_ALL & ~E_WARNING));

$this->expectException(DriverException::class);
$this->expectExceptionCode(4068);
}

// Create a pipelined funtion that returns 10 rows more than the
// oci8 default prefetch
$this->createReturnTypeNeededForPipelinedFunction();
$expectedTotalRowCount = (int) ini_get('oci8.default_prefetch') + 10;
$this->createOrReplacePipelinedFunction($expectedTotalRowCount);

// Create a separate connection from that used to create/update the function
// This must be a different user with permissions to change the given function
$separateConnection = TestUtil::getPrivilegedConnection();

// Query the pipelined function to get initial dataset
$statement = $separateConnection->prepare(sprintf(
'SELECT * FROM TABLE(%s.test_oracle_fetch_failure())',
$this->connectionParams['user']
));
$result = $statement->execute();

// Access the first result to cause the first X rows to be prefetched
// as defined by oci8.default_prefetch (often 100 rows)
$result->fetchOne();

if ($invalidateDataMidFetch) {
// Invalidate the original dataset by changing the pipelined function
// after the initial prefetch that caches locally the first X results
$this->createOrReplacePipelinedFunction($expectedTotalRowCount + 10);
}

while ($result->fetchOne()) {
// Attempt to access all remaining rows from the original fetch
// The rows locally cached from the default prefetch will first be used
// but when the result attempts to get the remaining 10 rows beyond
// the first prefetch, nothing will be returned
//
// PHP oci8 oci_fetch_array will issue a PHP E_WARNING when the 2nd prefetch occurs
// oci_fetch_array(): ORA-04068: existing state of packages has been discarded
// ORA-04061: existing state of function "ROOT.TEST_ORACLE_FETCH_FAILURE" has been invalidated
// ORA-04065: not executed, altered or dropped function "ROOT.TEST_ORACLE_FETCH_FAILURE"
//
// If there was no issue, this should have returned rows totalling 10
// higher than the oci8 default prefetch
continue;
}

self::assertEquals(
$expectedTotalRowCount,
$result->rowCount(),
sprintf(
'Expected to have %s total rows fetched but only found %s rows fetched',
$expectedTotalRowCount,
$result->rowCount()
)
);
}

public function dataProviderForTestTruncatedFetch(): Generator
{
yield 'it should return all rows if no data invalidation occurs'
=> [false];

yield 'it should convert oci8 data invalidation error to DriverException'
=> [true];
}

private function createReturnTypeNeededForPipelinedFunction(): void
{
$this->connection->executeQuery(
'CREATE TYPE return_numbers AS TABLE OF NUMBER(11)'
);
}

/**
* This will create a pipelined function that returns X rows with
* each row returning a single column_value of that row's row number.
* The total number of rows returned is equal to $totalRowCount.
*/
private function createOrReplacePipelinedFunction(int $totalRowCount): void
{
$this->connection->executeQuery(sprintf(
'CREATE OR REPLACE FUNCTION test_oracle_fetch_failure
RETURN return_numbers PIPELINED
AS
v_number_list return_numbers;
BEGIN
SELECT ROWNUM r
BULK COLLECT INTO v_number_list
FROM DUAL
CONNECT BY ROWNUM <= %d;
FOR i IN 1 .. v_number_list.COUNT
LOOP
PIPE ROW (v_number_list(i));
END LOOP;
RETURN;
END;',
$totalRowCount
));
}
}
38 changes: 38 additions & 0 deletions tests/Functional/StatementTest.php
Expand Up @@ -361,4 +361,42 @@ public function testExecWithRedundantParameters(): void

$stmt->execute([null]);
}

public function testExecuteQuery(): void
{
$platform = $this->connection->getDatabasePlatform();
$query = $platform->getDummySelectSQL();
$result = $this->connection->prepare($query)->executeQuery()->fetchOne();

self::assertEquals(1, $result);
}

public function testExecuteQueryWithParams(): void
{
$this->connection->insert('stmt_test', ['id' => 1]);

$query = 'SELECT id FROM stmt_test WHERE id = ?';
$result = $this->connection->prepare($query)->executeQuery([1])->fetchOne();

self::assertEquals(1, $result);
}

public function testExecuteStatement(): void
{
$this->connection->insert('stmt_test', ['id' => 1]);

$query = 'UPDATE stmt_test SET name = ? WHERE id = 1';
$stmt = $this->connection->prepare($query);

$stmt->bindValue(1, 'bar');

$result = $stmt->executeStatement();

self::assertEquals(1, $result);

$query = 'UPDATE stmt_test SET name = ? WHERE id = ?';
$result = $this->connection->prepare($query)->executeStatement(['foo', 1]);

self::assertEquals(1, $result);
}
}
17 changes: 17 additions & 0 deletions tests/Query/QueryBuilderTest.php
Expand Up @@ -849,6 +849,23 @@ public function testJoinWithNonUniqueAliasThrowsException(): void
$qb->getSQL();
}

public function testExecuteSelect(): void
{
$qb = new QueryBuilder($this->conn);

$this->conn
->expects(self::any())
->method('executeQuery')
->willReturn($this->createMock(Result::class));

$result = $qb
->select('id')
->from('foo')
->execute();

self::assertInstanceOf(Result::class, $result);
}

/**
* @param list<mixed>|array<string, mixed> $parameters
* @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $parameterTypes
Expand Down

0 comments on commit fcc469d

Please sign in to comment.