Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow start transaction during result iteration for MSSQL #1136

Merged
merged 9 commits into from
Oct 2, 2023
4 changes: 2 additions & 2 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ parameters:
count: 3

# FC for DBAL 4.0, remove once DBAL 3.x support is dropped
-
-
message: '~^Class Doctrine\\DBAL\\Platforms\\SqlitePlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\SQLitePlatform\.$~'
path: '*'
count: 33
count: 31

# TODO these rules are generated, this ignores should be fixed in the code
# for src/Schema/TestCase.php
Expand Down
46 changes: 31 additions & 15 deletions src/Persistence/Sql/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,13 @@ private function castGetValue($v): ?string
*/
public function getRowsIterator(): \Traversable
{
// DbalResult::iterateAssociative() is broken with streams with Oracle database
// https://github.com/doctrine/dbal/issues/5002
$iterator = $this->executeQuery()->iterateAssociative();
$result = $this->executeQuery();
if ($this->connection->getDatabasePlatform() instanceof SQLServerPlatform) {
// workaround MSSQL database limitation to allow to start transaction/savepoint in middle of result iteration
$iterator = $result->fetchAllAssociative();
} else {
$iterator = $result->iterateAssociative();
}

foreach ($iterator as $row) {
yield array_map(function ($v) {
Expand All @@ -729,18 +733,29 @@ public function getRowsIterator(): \Traversable
*/
public function getRows(): array
{
// DbalResult::fetchAllAssociative() is broken with streams with Oracle database
// https://github.com/doctrine/dbal/issues/5002
$result = $this->executeQuery();

$rows = [];
while (($row = $result->fetchAssociative()) !== false) {
$rows[] = array_map(function ($v) {
return $this->castGetValue($v);
}, $row);
if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
// DbalResult::fetchAllAssociative() is broken with streams with Oracle database
// https://github.com/doctrine/dbal/issues/5002

$res = [];
while (($row = $result->fetchAssociative()) !== false) {
$res[] = array_map(function ($v) {
return $this->castGetValue($v);
}, $row);
}

return $res;
}

return $rows;
$rows = $result->fetchAllAssociative();

return array_map(function ($row) {
return array_map(function ($v) {
return $this->castGetValue($v);
}, $row);
}, $rows);
}

/**
Expand All @@ -766,14 +781,15 @@ public function getRow(): ?array
*/
public function getOne(): ?string
{
$row = $this->getRow();
if ($row === null || count($row) === 0) {
throw (new Exception('Unable to fetch single cell of data for getOne from this query'))
$row = $this->executeQuery()->fetchAssociative();

if ($row === false || count($row) === 0) {
throw (new Exception('Unable to fetch single cell of data'))
->addMoreInfo('result', $row)
->addMoreInfo('query', $this->getDebugQuery());
}

return reset($row);
return $this->castGetValue(reset($row));
}

// }}}
Expand Down
9 changes: 6 additions & 3 deletions tests/Persistence/Sql/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Atk4\Data\Tests\Persistence\Sql;

use Atk4\Core\Phpunit\TestCase;
use Atk4\Data\Exception;
use Atk4\Data\Persistence;
use Atk4\Data\Persistence\Sql\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
Expand Down Expand Up @@ -134,7 +135,8 @@ public function testConnectionRegistry(): void
try {
Connection::resolveConnectionClass('dummy2');
self::assertFalse(true); // @phpstan-ignore-line
} catch (\Exception $e) {
} catch (Exception $e) {
self::assertSame('Driver schema is not registered', $e->getMessage());
}

Connection::registerConnectionClass(DummyConnection2::class, 'dummy2');
Expand All @@ -146,10 +148,11 @@ public function testConnectionRegistry(): void
}
}

public function testMysqlFail(): void
public function testConnectInvalidHostException(): void
{
$this->expectException(\Exception::class);
Connection::connect('mysql:host=256.256.256.256'); // invalid host
$this->expectExceptionMessage('An exception occurred in the driver: php_network_getaddresses');
Connection::connect('mysql:host=256.256.256.256');
}

public function testException1(): void
Expand Down
11 changes: 10 additions & 1 deletion tests/Persistence/Sql/WithDb/SelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,21 @@ public function testOtherQueries(): void
], $this->q('employee')->field('id')->field('name')->getRows());
}

public function testEmptyGetOne(): void
public function testGetRowEmpty(): void
{
$this->q('employee')->mode('truncate')->executeStatement();
$q = $this->q('employee');

self::assertNull($q->getRow());
}

public function testGetOneEmptyException(): void
{
$this->q('employee')->mode('truncate')->executeStatement();
$q = $this->q('employee')->field('name');

$this->expectException(Exception::class);
$this->expectExceptionMessage('Unable to fetch single cell of data');
$q->getOne();
}

Expand Down
43 changes: 16 additions & 27 deletions tests/Persistence/Sql/WithDb/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Atk4\Data\Persistence\Sql\Expression;
use Atk4\Data\Persistence\Sql\Query;
use Atk4\Data\Schema\TestCase;
use Doctrine\DBAL\Exception\InvalidFieldNameException;

class TransactionTest extends TestCase
{
Expand Down Expand Up @@ -99,8 +100,8 @@ public function testTransactions(): void
$this->q('employee')
->setMulti(['id' => 2, 'FOO' => 'bar', 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
} catch (\Exception $e) {
// ignore
} catch (Exception $e) {
self::assertInstanceOf(InvalidFieldNameException::class, $e->getPrevious());
}

self::assertSame(
Expand Down Expand Up @@ -134,8 +135,8 @@ public function testTransactions(): void
->setMulti(['id' => 4, 'FOO' => 'bar', 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
});
} catch (\Exception $e) {
// ignore
} catch (Exception $e) {
self::assertInstanceOf(InvalidFieldNameException::class, $e->getPrevious());
}

self::assertSame(
Expand All @@ -159,13 +160,9 @@ public function testTransactions(): void
->setMulti(['id' => 5, 'FOO' => 'bar', 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
});

$this->q('employee')
->setMulti(['id' => 6, 'FOO' => 'bar', 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
});
} catch (\Exception $e) {
// ignore
} catch (Exception $e) {
self::assertInstanceOf(InvalidFieldNameException::class, $e->getPrevious());
}

self::assertSame(
Expand All @@ -191,8 +188,8 @@ public function testTransactions(): void
->setMulti(['id' => 5, 'FOO' => 'bar', 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
});
} catch (\Exception $e) {
// ignore
} catch (Exception $e) {
self::assertInstanceOf(InvalidFieldNameException::class, $e->getPrevious());
}

self::assertSame(
Expand All @@ -213,13 +210,9 @@ public function testTransactions(): void
->setMulti(['id' => 4, 'FOO' => 'bar', 'name' => 'John', 'surname' => 'Doe', 'retired' => true])
->mode('insert')->executeStatement();
});

$this->q('employee')
->setMulti(['id' => 5, 'name' => 'Jane', 'surname' => 'Doe', 'retired' => false])
->mode('insert')->executeStatement();
});
} catch (\Exception $e) {
// ignore
} catch (Exception $e) {
self::assertInstanceOf(InvalidFieldNameException::class, $e->getPrevious());
}

self::assertSame(
Expand All @@ -228,15 +221,11 @@ public function testTransactions(): void
);

// atomic method, success - commit
try {
$this->getConnection()->atomic(function () {
$this->q('employee')
->setMulti(['id' => 3, 'name' => 'John', 'surname' => 'Doe', 'retired' => true])
->mode('insert')->executeStatement();
});
} catch (\Exception $e) {
// ignore
}
$this->getConnection()->atomic(function () {
$this->q('employee')
->setMulti(['id' => 3, 'name' => 'John', 'surname' => 'Doe', 'retired' => true])
->mode('insert')->executeStatement();
});

self::assertSame(
'2',
Expand Down
122 changes: 74 additions & 48 deletions tests/Schema/TestCaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

use Atk4\Data\Model;
use Atk4\Data\Schema\TestCase;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;

class TestCaseTest extends TestCase
{
Expand Down Expand Up @@ -37,53 +38,78 @@ public function testLogQuery(): void
ob_end_clean();
}

if (!$this->getDatabasePlatform() instanceof SQLitePlatform && !$this->getDatabasePlatform() instanceof MySQLPlatform) {
return;
}
$makeLimitSqlFx = function (int $maxCount) {
if ($this->getDatabasePlatform() instanceof PostgreSQLPlatform) {
return "\nlimit\n " . $maxCount . ' offset 0';
} elseif ($this->getDatabasePlatform() instanceof SQLServerPlatform) {
return "\norder by\n (\n select\n null\n ) offset 0 rows fetch next " . $maxCount . ' rows only';
} elseif ($this->getDatabasePlatform() instanceof OraclePlatform) {
return ' fetch next ' . $maxCount . ' rows only';
}

$this->assertSameSql(<<<'EOF'

"START TRANSACTION";


insert into `t` (`name`, `int`, `float`, `null`)
values
('Ewa', 1, '1.0', NULL);


select
`id`,
`name`,
`int`,
`float`,
`null`
from
`t`
where
`int` > -1
and `id` = 1
limit
0,
2;


"COMMIT";


select
`id`,
`name`,
`int`,
`float`,
`null`
from
`t`
where
`int` > -1
limit
0,
1;
EOF . "\n\n", $output);
return "\nlimit\n 0,\n " . $maxCount;
};

$this->assertSameSql(
<<<'EOF'

"START TRANSACTION";


insert into `t` (`name`, `int`, `float`, `null`)
values
(
EOF
. ($this->getDatabasePlatform() instanceof OraclePlatform ? "\n " : '')
. '\'Ewa\', 1, \'1.0\', NULL'
. ($this->getDatabasePlatform() instanceof OraclePlatform ? "\n " : '')
. ");\n\n"
. ($this->getDatabasePlatform() instanceof OraclePlatform ? <<<'EOF'

select
"t_SEQ".CURRVAL
from
"DUAL";
EOF . "\n\n" : '')
. <<<'EOF'

select
`id`,
`name`,
`int`,
`float`,
`null`
from
`t`
where
`int` > -1
and `id` = 1
EOF
. $makeLimitSqlFx(2)
. <<<'EOF'
;


"COMMIT";


select
`id`,
`name`,
`int`,
`float`,
`null`
from
`t`
where
`int` > -1
EOF
. $makeLimitSqlFx(1)
. ";\n\n",
$this->getDatabasePlatform() instanceof SQLServerPlatform
? str_replace('(\'Ewa\', 1, \'1.0\', NULL)', '(N\'Ewa\', 1, N\'1.0\', NULL)', $output)
: $output
);
}

public function testGetSetDropDb(): void
Expand Down
Loading