ResultSet::normalizeRow() calls strpos() on float with native prepares
spaze opened this issue · 0 comments
Version: 3.0.1
Bug Description
ResultSet::normalizeRow()
calls strpos()
on float with native prepares (non-default in MySQL driver) and throws
TypeError: strpos() expects parameter 1 to be string, float given in src/Database/ResultSet.php(143)
Steps To Reproduce
-
Run your app in debug mode, check you have the Tracy bar visible and it displays the database panel:
-
(Re)configure the database service to use native prepared statements:
database:
dsn: ...
user: ...
password: ...
options:
PDO::ATTR_EMULATE_PREPARES: false
The stack trace:
#0 .../src/Database/ResultSet.php(143): strpos(100, '.')
#1 .../src/Database/ResultSet.php(240): Nette\Database\ResultSet->normalizeRow(Array)
#2 .../src/Database/ResultSet.php(217): Nette\Database\ResultSet->fetch()
#3 [internal function]: Nette\Database\ResultSet->valid()
#4 .../src/Database/ResultSet.php(289): iterator_to_array(Object(Nette\Database\ResultSet))
#5 .../src/Bridges/DatabaseTracy/ConnectionPanel.php(130): Nette\Database\ResultSet->fetchAll()
normalizeRow()
calls strpos(100, '.')
which will fail with declare(strict_types = 1)
, because strpos
expects parameter 1 to be a string but here we have a float/int. The number 100 comes from an EXPLAIN
query which is executed by the debugger panel, it's in a column called filtered
.
The root cause is that when PDO uses emulated prepares (the default in PDO_MYSQL), then numbers (ints, floats) are returned as string and normalizeRow()
fixes it back to numbers. But when PDO uses native prepared statements, numbers are returned as numbers, spot the difference:
>>> $stmt = (new PDO($dsn, $u, $p, [PDO::ATTR_EMULATE_PREPARES => false]))->prepare('SELECT id_talk FROM talks WHERE id_talk = 1'); $stmt->execute(); $stmt->fetch();
=> [
"id_talk" => 1,
0 => 1,
]
>>> $stmt = (new PDO($dsn, $u, $p, [PDO::ATTR_EMULATE_PREPARES => true]))->prepare('SELECT id_talk FROM talks WHERE id_talk = 1'); $stmt->execute(); $stmt->fetch();
=> [
"id_talk" => "1",
0 => "1",
]
This is the failing part in ResultSet
, where $type
comes from metadata but $value
is string with emulated prepares but float with native:
} elseif ($type === IStructure::FIELD_FLOAT) {
if (($pos = strpos($value, '.')) !== false) {
$value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.');
}
$float = (float) $value;
$row[$key] = (string) $float === $value ? $float : $value;
Expected Behavior
The debugger panel displays queries and not an exception
Possible Solution
The easiest is to cast $value
to string to make sure strpos
always gets string no matter what prepares are used:
} elseif ($type === IStructure::FIELD_FLOAT) {
if (($pos = strpos((string)$value, '.')) !== false) {
$value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.');
}
And the same for the rtrim
below of course. I'll prepare a PR with a test.
Thanks.