From 21deb140e8532e765b1df61498449fe02991cafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kr=C3=BCss?= Date: Tue, 3 Nov 2026 10:29:11 -0700 Subject: [PATCH] Fix Relay SELECT with a non-integer database and add Relay range test (#1751) Relay::select() throws a TypeError for anything but an integer, where other connections get an error response from the server. Wrap the call and return the same error response, so the existing test runs against Relay and no longer needs a Relay-only counterpart. Relay warns before sending SELECT for out-of-range databases, so that test keeps its Relay-specific counterpart. --- CHANGELOG.md | 1 + src/Connection/RelayConnection.php | 25 +++++++ tests/Predis/Command/Redis/SELECT_Test.php | 35 +++++++++- .../Predis/Connection/RelayConnectionTest.php | 68 +++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f78c9ee..d296a0bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed Relay returning server errors as error responses and raw error messages without stack traces (#1292) - Fixed Relay reconnecting after an explicit disconnect (#1292) - Fixed Relay ACL authentication accepting username and password (#1292) +- Fixed Relay throwing a `TypeError` instead of returning an error response when selecting a non-integer database (#1751) - Fixed `INFO` parsing warnings with Valkey 9 (#1748) ## v3.7.0 (2026-10-08) diff --git a/src/Connection/RelayConnection.php b/src/Connection/RelayConnection.php index 5d4d462d..ecba1988 100644 --- a/src/Connection/RelayConnection.php +++ b/src/Connection/RelayConnection.php @@ -12,10 +12,13 @@ namespace Predis\Connection; +use TypeError; +use ArgumentCountError; use InvalidArgumentException; use Predis\ClientException; use Predis\Command\CommandInterface; use Predis\NotSupportedException; +use Predis\Response\Error as ErrorResponse; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ServerException; use Relay\Exception as RelayException; @@ -204,6 +207,10 @@ class RelayConnection extends AbstractConnection $arguments = [$arguments]; } + if ($name === 'SELECT') { + return $this->select($arguments); + } + // Raw dispatch leaves values untouched. Serialization and compression // are applied explicitly with pack() and unpack(). return in_array($name, $this->atypicalCommands) @@ -222,6 +229,24 @@ class RelayConnection extends AbstractConnection } } + /** + * Relay's select() throws a TypeError for a non-integer database, where + * other connections get an error response from the server. + * + * @param array $arguments + * @return mixed + */ + protected function select(array $arguments) + { + try { + return $this->client->select(...$arguments); + } catch (ArgumentCountError $ex) { + throw $ex; + } catch (TypeError $ex) { + return new ErrorResponse('ERR value is not an integer or out of range'); + } + } + /** * {@inheritdoc} */ diff --git a/tests/Predis/Command/Redis/SELECT_Test.php b/tests/Predis/Command/Redis/SELECT_Test.php index 551c72c7..76e223c0 100644 --- a/tests/Predis/Command/Redis/SELECT_Test.php +++ b/tests/Predis/Command/Redis/SELECT_Test.php @@ -83,9 +83,42 @@ class SELECT_Test extends PredisCommandTestCase $redis->select(100000000); } + /** + * Relay warns before sending SELECT for indexes beyond `relay.databases`, + * which PHPUnit would turn into an exception before the server replies. + * + * @group connected + */ + public function testThrowsExceptionOnUnexpectedDatabaseRangeDespiteRelayWarning(): void + { + if (!getenv('USE_RELAY')) { + $this->markTestSkipped('Relay only warns when it is the active connection.'); + } + + $this->expectException('Predis\Response\ServerException'); + $this->expectExceptionMessageMatches('/ERR.*DB index/'); + + $redis = $this->getClient(); + + $previous = set_error_handler( + static function ($level, $message, $file = '', $line = 0) use (&$previous) { + if (strpos($message, 'in-memory databases') !== false) { + return true; + } + + return $previous ? $previous($level, $message, $file, $line) : false; + } + ); + + try { + $redis->select(100000000); + } finally { + restore_error_handler(); + } + } + /** * @group connected - * @group relay-incompatible */ public function testThrowsExceptionOnUnexpectedDatabaseName(): void { diff --git a/tests/Predis/Connection/RelayConnectionTest.php b/tests/Predis/Connection/RelayConnectionTest.php index 309e48cb..7c4b1b3a 100644 --- a/tests/Predis/Connection/RelayConnectionTest.php +++ b/tests/Predis/Connection/RelayConnectionTest.php @@ -12,6 +12,7 @@ namespace Predis\Connection; +use ArgumentCountError; use PHPUnit\Framework\MockObject\MockObject; use Predis\Client; use Predis\ClientException; @@ -24,6 +25,7 @@ use PredisTestCase; use ReflectionClass; use Relay\Exception as RelayException; use Relay\Relay; +use TypeError; /** * @group ext-relay @@ -166,6 +168,72 @@ class RelayConnectionTest extends PredisTestCase $this->assertTrue($response); } + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectOnClient(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->expects($this->once()) + ->method('SELECT') + ->with(1) + ->willReturn(true); + + $response = $this->connection->executeCommand(new RawCommand('SELECT', [1])); + + $this->assertTrue($response); + } + + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectReturnsErrorResponseOnNonIntegerDatabase(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->method('SELECT') + ->willThrowException(new TypeError('Argument #1 ($db) must be of type int, string given')); + + $response = $this->connection->executeCommand(new RawCommand('SELECT', ['x'])); + + $this->assertInstanceOf(ErrorResponse::class, $response); + $this->assertSame('ERR value is not an integer or out of range', (string) $response); + } + + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectDoesNotHideArgumentCountError(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->method('SELECT') + ->willThrowException(new ArgumentCountError('Too few arguments')); + + $this->expectException(ArgumentCountError::class); + + $this->connection->executeCommand(new RawCommand('SELECT', [])); + } + /** * @group disconnected * @return void