Skip to content

Commit 9a3aeaf

Browse files
bug #62799 [Cache][HttpFoundation] Fix VARBINARY columns on sqlsrv (nicolas-grekas)
This PR was merged into the 6.4 branch. Discussion ---------- [Cache][HttpFoundation] Fix VARBINARY columns on sqlsrv | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #62241 | License | MIT PdoSessionHandler was failing against SQL Azure when writing session data to a VARBINARY(MAX) column because the pdo_sqlsrv driver sent the data as nvarchar, which SQL Server cannot implicitly convert to varbinary(max). This patch makes the sqlsrv driver use a stream resource for session data (like the existing oci handling), both for INSERT/UPDATE statements and the MERGE upsert path. This ensures the data is treated as binary and avoids the implicit conversion error, while keeping the existing schema (VARBINARY(MAX)) unchanged. A new test (testSqlsrvDataBindingUsesStream) verifies that, for sqlsrv, the session data is bound as a PDO::PARAM_LOB resource when using the MERGE path. Commits ------- af745d4 [HttpFoundation][Cache] Fix VARBINARY columns on sqlsrv
2 parents e10c89d + af745d4 commit 9a3aeaf

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

src/Symfony/Component/Cache/Adapter/PdoAdapter.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,17 @@ protected function doSave(array $values, int $lifetime): array|bool
314314
$insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
315315
}
316316

317+
if ('sqlsrv' === $driver) {
318+
$dataStream = fopen('php://memory', 'r+');
319+
}
317320
foreach ($values as $id => $data) {
321+
if ('sqlsrv' === $driver) {
322+
rewind($dataStream);
323+
fwrite($dataStream, $data);
324+
ftruncate($dataStream, \strlen($data));
325+
rewind($dataStream);
326+
$data = $dataStream;
327+
}
318328
try {
319329
$stmt->execute();
320330
} catch (\PDOException $e) {

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,12 @@ private function getInsertStatement(#[\SensitiveParameter] string $sessionId, st
795795
rewind($data);
796796
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
797797
break;
798+
case 'sqlsrv':
799+
$data = fopen('php://memory', 'r+');
800+
fwrite($data, $sessionData);
801+
rewind($data);
802+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
803+
break;
798804
default:
799805
$data = $sessionData;
800806
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
@@ -822,6 +828,12 @@ private function getUpdateStatement(#[\SensitiveParameter] string $sessionId, st
822828
rewind($data);
823829
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
824830
break;
831+
case 'sqlsrv':
832+
$data = fopen('php://memory', 'r+');
833+
fwrite($data, $sessionData);
834+
rewind($data);
835+
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
836+
break;
825837
default:
826838
$data = $sessionData;
827839
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
@@ -869,12 +881,16 @@ private function getMergeStatement(#[\SensitiveParameter] string $sessionId, str
869881
$mergeStmt = $this->pdo->prepare($mergeSql);
870882

871883
if ('sqlsrv' === $this->driver) {
884+
$dataStream = fopen('php://memory', 'r+');
885+
fwrite($dataStream, $data);
886+
rewind($dataStream);
887+
872888
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
873889
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
874-
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
890+
$mergeStmt->bindParam(3, $dataStream, \PDO::PARAM_LOB);
875891
$mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
876892
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
877-
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
893+
$mergeStmt->bindParam(6, $dataStream, \PDO::PARAM_LOB);
878894
$mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT);
879895
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
880896
} else {

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,46 @@ public function testTtl()
390390
}
391391
}
392392

393+
public function testSqlsrvDataBindingUsesStream()
394+
{
395+
$pdo = new MockPdo('sqlsrv', null, '10');
396+
$boundData = [];
397+
398+
$mergeStmt = $this->createMock(\PDOStatement::class);
399+
$selectStmt = $this->createMock(\PDOStatement::class);
400+
$selectStmt->method('fetchAll')->willReturn([]);
401+
402+
$mergeStmt->method('bindParam')
403+
->willReturnCallback(function ($param, $data, $type = null) use (&$boundData) {
404+
$boundData[$param] = ['data' => $data, 'type' => $type];
405+
406+
return true;
407+
});
408+
409+
$mergeStmt->method('bindValue')->willReturn(true);
410+
$mergeStmt->method('execute')->willReturn(true);
411+
412+
$pdo->prepareResult = fn ($statement) => str_starts_with($statement, 'MERGE') ? $mergeStmt : $selectStmt;
413+
414+
$storage = new PdoSessionHandler($pdo, ['lock_mode' => PdoSessionHandler::LOCK_NONE]);
415+
$storage->open('', 'sid');
416+
$storage->read('id');
417+
$storage->write('id', 'test_data');
418+
$storage->close();
419+
420+
$this->assertArrayHasKey(3, $boundData);
421+
$this->assertIsResource($boundData[3]['data']);
422+
$this->assertSame(\PDO::PARAM_LOB, $boundData[3]['type']);
423+
rewind($boundData[3]['data']);
424+
$this->assertSame('test_data', stream_get_contents($boundData[3]['data']));
425+
426+
$this->assertArrayHasKey(6, $boundData);
427+
$this->assertIsResource($boundData[6]['data']);
428+
$this->assertSame(\PDO::PARAM_LOB, $boundData[6]['type']);
429+
rewind($boundData[6]['data']);
430+
$this->assertSame('test_data', stream_get_contents($boundData[6]['data']));
431+
}
432+
393433
/**
394434
* @return resource
395435
*/
@@ -408,11 +448,13 @@ class MockPdo extends \PDO
408448
public \Closure|\PDOStatement|false $prepareResult;
409449
private ?string $driverName;
410450
private bool|int $errorMode;
451+
private ?string $serverVersion;
411452

412-
public function __construct(?string $driverName = null, ?int $errorMode = null)
453+
public function __construct(?string $driverName = null, ?int $errorMode = null, ?string $serverVersion = null)
413454
{
414455
$this->driverName = $driverName;
415456
$this->errorMode = null !== $errorMode ?: \PDO::ERRMODE_EXCEPTION;
457+
$this->serverVersion = $serverVersion;
416458
}
417459

418460
public function getAttribute($attribute): mixed
@@ -425,6 +467,10 @@ public function getAttribute($attribute): mixed
425467
return $this->driverName;
426468
}
427469

470+
if (\PDO::ATTR_SERVER_VERSION === $attribute) {
471+
return $this->serverVersion;
472+
}
473+
428474
return parent::getAttribute($attribute);
429475
}
430476

0 commit comments

Comments
 (0)