Skip to content

Commit 06d6249

Browse files
authored
Store binary values natively for PostgreSQL and MSSQL (#921)
* Store binary values natively for PostgreSQL * Fix DBAL PDO connection create for MSSQL * Store binary values natively for MSSQL
1 parent ebfe694 commit 06d6249

7 files changed

+46
-28
lines changed

phpstan.neon.dist

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ parameters:
2828
# for Doctrine DBAL 2.x, remove the support once Doctrine ORM 2.10 is released
2929
# see https://github.com/doctrine/orm/issues/8526
3030
-
31-
message: '~^(Call to an undefined method Doctrine\\DBAL\\Driver\\Connection::getWrappedConnection\(\)\.|Caught class Doctrine\\DBAL\\DBALException not found\.|Call to static method notSupported\(\) on an unknown class Doctrine\\DBAL\\DBALException\.|Access to an undefined property Doctrine\\DBAL\\Driver\\PDO\\Connection::\$connection\.|Method Atk4\\Data\\Persistence\\Sql\\Expression::execute\(\) should return Doctrine\\DBAL\\Result\|PDOStatement but returns bool\.|Class Doctrine\\DBAL\\Platforms\\MySqlPlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\MySQLPlatform\.)$~'
31+
message: '~^(Call to an undefined method Doctrine\\DBAL\\Driver\\Connection::getWrappedConnection\(\)\.|Caught class Doctrine\\DBAL\\DBALException not found\.|Call to static method notSupported\(\) on an unknown class Doctrine\\DBAL\\DBALException\.|Access to an undefined property Doctrine\\DBAL\\Driver\\PDO\\Connection::\$connection\.|Parameter #1 \$dsn of class Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection constructor expects string, Doctrine\\DBAL\\Driver\\PDO\\Connection given\.|Method Atk4\\Data\\Persistence\\Sql\\Expression::execute\(\) should return Doctrine\\DBAL\\Result\|PDOStatement but returns bool\.|Class Doctrine\\DBAL\\Platforms\\MySqlPlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\MySQLPlatform\.)$~'
3232
path: '*'
3333
# count for DBAL 3.x matched in "src/Persistence/GenericPlatform.php" file
34-
count: 11
34+
count: 12
3535

3636
# TODO these rules are generated, this ignores should be fixed in the code
3737
# for src/Schema/TestCase.php

src/Persistence/GenericPlatform.php

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ private function createNotSupportedException(): \Exception // DbalException once
2222
\Doctrine\DBAL\DBALException::notSupported('SQL');
2323
\Doctrine\DBAL\DBALException::notSupported('SQL');
2424
\Doctrine\DBAL\DBALException::notSupported('SQL');
25+
\Doctrine\DBAL\DBALException::notSupported('SQL');
2526
}
2627

2728
return \Doctrine\DBAL\DBALException::notSupported('SQL');

src/Persistence/Sql/BinaryTypeCompatibilityTypecastTrait.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,19 @@ private function binaryTypeValueDecode(string $value): string
4242
throw new Exception('Unexpected binary value crc');
4343
}
4444

45-
return hex2bin($hex);
45+
$res = hex2bin($hex);
46+
if ($this->binaryTypeValueIsEncoded($res)) {
47+
throw new Exception('Unexpected double encoded binary value');
48+
}
49+
50+
return $res;
4651
}
4752

4853
private function binaryTypeIsEncodeNeeded(Type $type): bool
4954
{
50-
// TODO PostgreSQL tests fail without binary compatibility typecast
55+
// binary values for PostgreSQL and MSSQL databases are stored natively, but we need
56+
// to encode first to hold the binary type info for PDO parameter type binding
57+
5158
$platform = $this->getDatabasePlatform();
5259
if ($platform instanceof PostgreSQL94Platform
5360
|| $platform instanceof SQLServer2012Platform
@@ -76,7 +83,11 @@ public function typecastLoadField(Field $field, $value)
7683
$value = parent::typecastLoadField($field, $value);
7784

7885
if ($value !== null && $this->binaryTypeIsEncodeNeeded($field->getTypeObject())) {
79-
$value = $this->binaryTypeValueDecode($value);
86+
// always decode for Oracle platform to assert the value is always encoded,
87+
// on other platforms, binary values are stored natively
88+
if ($this->getDatabasePlatform() instanceof OraclePlatform || $this->binaryTypeValueIsEncoded($value)) {
89+
$value = $this->binaryTypeValueDecode($value);
90+
}
8091
}
8192

8293
return $value;

src/Persistence/Sql/Connection.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,22 @@ public static function connect($dsn, $user = null, $password = null, $args = [])
132132
// If it's already PDO or DbalConnection object, then we simply use it
133133
if ($dsn instanceof \PDO) {
134134
$connectionClass = self::resolveConnectionClass($dsn->getAttribute(\PDO::ATTR_DRIVER_NAME));
135-
$connectionArg = $connectionClass::connectDbalConnection(['pdo' => $dsn]);
135+
$dbalConnection = $connectionClass::connectDbalConnection(['pdo' => $dsn]);
136136
} elseif ($dsn instanceof DbalConnection) {
137137
/** @var \PDO */
138138
$pdo = self::isComposerDbal2x()
139139
? $dsn->getWrappedConnection()
140140
: $dsn->getWrappedConnection()->getWrappedConnection();
141141
$connectionClass = self::resolveConnectionClass($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME));
142-
$connectionArg = $dsn;
142+
$dbalConnection = $dsn;
143143
} else {
144144
$dsn = static::normalizeDsn($dsn, $user, $password);
145145
$connectionClass = self::resolveConnectionClass($dsn['driverSchema']);
146-
$connectionArg = $connectionClass::connectDbalConnection($dsn);
146+
$dbalConnection = $connectionClass::connectDbalConnection($dsn);
147147
}
148148

149149
return new $connectionClass(array_merge([
150-
'connection' => $connectionArg,
150+
'connection' => $dbalConnection,
151151
], $args));
152152
}
153153

@@ -241,8 +241,13 @@ protected static function connectDbalConnection(array $dsn)
241241
\Closure::bind(function () use ($pdoConnection, $pdo): void {
242242
$pdoConnection->connection = $pdo;
243243
}, null, \Doctrine\DBAL\Driver\PDO\Connection::class)();
244+
$pdoAttrDriverName = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
245+
if ($pdoAttrDriverName === 'sqlsrv') {
246+
$pdoConnection = new \Doctrine\DBAL\Driver\PDO\SQLSrv\Connection($pdoConnection);
247+
}
248+
244249
$dbalConnection = DriverManager::getConnection([
245-
'driver' => 'pdo_' . $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME),
250+
'driver' => 'pdo_' . $pdoAttrDriverName,
246251
], null, (static::class)::createDbalEventManager());
247252
\Closure::bind(function () use ($dbalConnection, $pdoConnection): void {
248253
$dbalConnection->_conn = $pdoConnection;

src/Persistence/Sql/Expression.php

+11
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
namespace Atk4\Data\Persistence\Sql;
66

77
use Atk4\Core\WarnDynamicPropertyTrait;
8+
use Atk4\Data\Persistence\Sql as SqlPersistence;
89
use Doctrine\DBAL\Connection as DbalConnection;
910
use Doctrine\DBAL\Exception as DbalException;
1011
use Doctrine\DBAL\ParameterType;
1112
use Doctrine\DBAL\Platforms\OraclePlatform;
1213
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
14+
use Doctrine\DBAL\Platforms\SQLServer2012Platform;
1315
use Doctrine\DBAL\Result as DbalResult;
1416

1517
/**
@@ -528,6 +530,15 @@ public function execute(object $connection = null): object
528530
$type = ParameterType::STRING;
529531
} elseif (is_string($val)) {
530532
$type = ParameterType::STRING;
533+
534+
if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform
535+
|| $this->connection->getDatabasePlatform() instanceof SQLServer2012Platform) {
536+
$dummySqlPersistence = new SqlPersistence($this->connection);
537+
if (\Closure::bind(fn () => $dummySqlPersistence->binaryTypeValueIsEncoded($val), null, SqlPersistence::class)()) {
538+
$val = \Closure::bind(fn () => $dummySqlPersistence->binaryTypeValueDecode($val), null, SqlPersistence::class)();
539+
$type = ParameterType::BINARY;
540+
}
541+
}
531542
} elseif (is_resource($val)) {
532543
throw new Exception('Resource type is not supported, set value as string instead');
533544
} else {

src/Persistence/Sql/Mssql/PlatformTrait.php

-15
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,6 @@
66

77
trait PlatformTrait
88
{
9-
// SQL Server database requires explicit conversion when using binary column,
10-
// workaround by using a standard non-binary column with custom encoding/typecast
11-
12-
protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
13-
{
14-
return $this->getVarcharTypeDeclarationSQLSnippet($length, $fixed);
15-
}
16-
17-
public function getBlobTypeDeclarationSQL(array $column)
18-
{
19-
return $this->getClobTypeDeclarationSQL($column);
20-
}
21-
229
// remove once https://github.com/doctrine/dbal/pull/4987 is fixed
2310
// and also $this->markDoctrineTypeCommented('text') below
2411
public function getClobTypeDeclarationSQL(array $column)
@@ -32,8 +19,6 @@ protected function initializeCommentedDoctrineTypes()
3219
{
3320
parent::initializeCommentedDoctrineTypes();
3421

35-
$this->markDoctrineTypeCommented('binary');
36-
$this->markDoctrineTypeCommented('blob');
3722
$this->markDoctrineTypeCommented('text');
3823
}
3924

tests/Schema/ModelTest.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Atk4\Data\Model;
88
use Atk4\Data\Schema\TestCase;
99
use Doctrine\DBAL\Platforms\OraclePlatform;
10+
use Doctrine\DBAL\Platforms\SQLServer2012Platform;
1011

1112
class ModelTest extends TestCase
1213
{
@@ -186,7 +187,7 @@ private function makePseudoRandomString(bool $isBinary, int $lengthBytes): strin
186187
public function testCharacterTypeFieldLong(string $type, bool $isBinary, int $lengthBytes): void
187188
{
188189
if ($this->getDatabasePlatform() instanceof OraclePlatform) {
189-
$lengthBytes = min($lengthBytes, 500);
190+
$lengthBytes = min($lengthBytes, $type === 'binary' ? 100 : 500);
190191
}
191192

192193
$str = $this->makePseudoRandomString($isBinary, $lengthBytes);
@@ -200,7 +201,11 @@ public function testCharacterTypeFieldLong(string $type, bool $isBinary, int $le
200201

201202
$this->createMigrator($model)->create();
202203

203-
$model->import([['v' => $str . ($isBinary ? "\0" : '.')]]);
204+
$model->import([['v' => $str . (
205+
// MSSQL database ignores trailing \0 characters even with binary comparison
206+
// https://dba.stackexchange.com/questions/48660/comparing-binary-0x-and-0x00-turns-out-to-be-equal-on-sql-server
207+
$isBinary ? $this->getDatabasePlatform() instanceof SQLServer2012Platform ? ' ' : "\0" : '.'
208+
)]]);
204209
$model->import([['v' => $str]]);
205210

206211
$model->addCondition('v', $str);
@@ -218,7 +223,7 @@ public function providerCharacterTypeFieldLongData(): array
218223
{
219224
return [
220225
['string', false, 250],
221-
['binary', true, 100],
226+
['binary', true, 250],
222227
['text', false, 256 * 1024],
223228
['blob', true, 256 * 1024],
224229
];

0 commit comments

Comments
 (0)