Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/database-pgsql/src/Connection/PgSqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -180,13 +181,28 @@ public function prepare(

/**
* @param array<int|string, mixed> $bindings
*
* @throws ConnectionException
*/
private function bindValues(
PDOStatement $statement,
array $bindings,
): void {
foreach ($bindings as $key => $value) {
$param = is_int($key) ? $key + 1 : $key;

if (is_array($value)) {
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;
}

$type = match (true) {
is_bool($value) => PDO::PARAM_BOOL,
is_null($value) => PDO::PARAM_NULL,
Expand Down
13 changes: 13 additions & 0 deletions packages/database-pgsql/src/Exceptions/ConnectionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Marko\Database\PgSql\Exceptions;

use JsonException;
use Marko\Core\Exceptions\MarkoException;
use PDOException;

Expand All @@ -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,
);
}
}
52 changes: 52 additions & 0 deletions packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -685,4 +685,56 @@ 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');
});

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'");
});
});