From 0fe69fddaf8ac65ab3c737803987348ec376103c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Biarda?= <1135380+michalbiarda@users.noreply.github.com> Date: Thu, 14 May 2026 09:46:40 +0200 Subject: [PATCH 1/2] fix(database-pgsql): JSON-encode array bindings before passing to PDO PDO silently casts PHP arrays to the string "Array" when bound as query parameters. For json/jsonb columns this causes an "invalid input syntax for type json" error at the database level. Co-Authored-By: Claude Sonnet 4.6 --- .../src/Connection/PgSqlConnection.php | 6 ++++ .../tests/Connection/PgSqlConnectionTest.php | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/database-pgsql/src/Connection/PgSqlConnection.php b/packages/database-pgsql/src/Connection/PgSqlConnection.php index 2edb610d..9300b521 100644 --- a/packages/database-pgsql/src/Connection/PgSqlConnection.php +++ b/packages/database-pgsql/src/Connection/PgSqlConnection.php @@ -187,6 +187,12 @@ private function bindValues( ): void { foreach ($bindings as $key => $value) { $param = is_int($key) ? $key + 1 : $key; + + if (is_array($value)) { + $statement->bindValue($param, json_encode($value), PDO::PARAM_STR); + continue; + } + $type = match (true) { is_bool($value) => PDO::PARAM_BOOL, is_null($value) => PDO::PARAM_NULL, diff --git a/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php b/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php index 3ab8c1cd..c68a86fc 100644 --- a/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php +++ b/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php @@ -685,4 +685,33 @@ protected function createPdo( expect(fn () => $connection->beginTransaction()) ->toThrow(TransactionException::class, 'Nested transactions are not supported'); }); + + it('JSON-encodes array bindings instead of casting them to the string "Array"', function (): void { + $config = createTestPgSqlConfig(); + $connection = new class ($config) extends PgSqlConnection + { + protected function createPdo( + string $dsn, + string $username, + string $password, + array $options, + ): PDO { + $pdo = createSqliteMockPdo($options); + $pdo->exec('CREATE TABLE items (id INTEGER PRIMARY KEY, metadata TEXT)'); + + return $pdo; + } + }; + + $connection->execute( + 'INSERT INTO items (metadata) VALUES (?)', + [['key' => 'value', 'nested' => [1, 2, 3]]], + ); + + $rows = $connection->query('SELECT metadata FROM items'); + + expect($rows[0]['metadata']) + ->toBe('{"key":"value","nested":[1,2,3]}') + ->not->toBe('Array'); + }); }); From 67123e4927b85d4d90cab79e83d5c556397bf3ba Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Sun, 24 May 2026 11:43:38 -0400 Subject: [PATCH 2/2] fix(database-pgsql): use JSON_THROW_ON_ERROR for array bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare json_encode() silently returns false on encoding failures (NAN, INF, recursive references, malformed UTF-8) and emits a PHP warning, which violates Marko's loud-errors principle. Match the established EntityHydrator::encodeJson() pattern: pass JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE, catch JsonException, and convert to a ConnectionException with a clear message identifying the offending parameter. Co-Authored-By: MichaƂ Biarda <1135380+michalbiarda@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/Connection/PgSqlConnection.php | 12 +++++++++- .../src/Exceptions/ConnectionException.php | 13 +++++++++++ .../tests/Connection/PgSqlConnectionTest.php | 23 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/database-pgsql/src/Connection/PgSqlConnection.php b/packages/database-pgsql/src/Connection/PgSqlConnection.php index 9300b521..5395527d 100644 --- a/packages/database-pgsql/src/Connection/PgSqlConnection.php +++ b/packages/database-pgsql/src/Connection/PgSqlConnection.php @@ -4,6 +4,7 @@ namespace Marko\Database\PgSql\Connection; +use JsonException; use Marko\Database\Config\DatabaseConfig; use Marko\Database\Connection\ConnectionInterface; use Marko\Database\Connection\StatementInterface; @@ -180,6 +181,8 @@ public function prepare( /** * @param array $bindings + * + * @throws ConnectionException */ private function bindValues( PDOStatement $statement, @@ -189,7 +192,14 @@ private function bindValues( $param = is_int($key) ? $key + 1 : $key; if (is_array($value)) { - $statement->bindValue($param, json_encode($value), PDO::PARAM_STR); + try { + $encoded = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); + } catch (JsonException $e) { + throw ConnectionException::invalidArrayBinding($param, $e); + } + + $statement->bindValue($param, $encoded, PDO::PARAM_STR); + continue; } diff --git a/packages/database-pgsql/src/Exceptions/ConnectionException.php b/packages/database-pgsql/src/Exceptions/ConnectionException.php index 94d7ab2d..8b025127 100644 --- a/packages/database-pgsql/src/Exceptions/ConnectionException.php +++ b/packages/database-pgsql/src/Exceptions/ConnectionException.php @@ -4,6 +4,7 @@ namespace Marko\Database\PgSql\Exceptions; +use JsonException; use Marko\Core\Exceptions\MarkoException; use PDOException; @@ -25,4 +26,16 @@ public static function connectionFailed( previous: $previous, ); } + + public static function invalidArrayBinding( + int|string $parameter, + JsonException $previous, + ): self { + return new self( + message: "Failed to JSON-encode array bound to parameter '$parameter'", + context: $previous->getMessage(), + suggestion: 'Ensure array values are JSON-encodable (no resources, recursive references, NAN, or INF)', + previous: $previous, + ); + } } diff --git a/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php b/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php index c68a86fc..9316a6d8 100644 --- a/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php +++ b/packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php @@ -714,4 +714,27 @@ protected function createPdo( ->toBe('{"key":"value","nested":[1,2,3]}') ->not->toBe('Array'); }); + + it('throws ConnectionException when an array binding is not JSON-encodable', function (): void { + $config = createTestPgSqlConfig(); + $connection = new class ($config) extends PgSqlConnection + { + protected function createPdo( + string $dsn, + string $username, + string $password, + array $options, + ): PDO { + $pdo = createSqliteMockPdo($options); + $pdo->exec('CREATE TABLE items (id INTEGER PRIMARY KEY, metadata TEXT)'); + + return $pdo; + } + }; + + expect(fn () => $connection->execute( + 'INSERT INTO items (metadata) VALUES (?)', + [[NAN]], + ))->toThrow(ConnectionException::class, "Failed to JSON-encode array bound to parameter '1'"); + }); });