nette/database

ResultSet::normalizeRow() calls strpos() on float with native prepares

spaze opened this issue · 0 comments

spaze commented

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

  1. Run your app in debug mode, check you have the Tracy bar visible and it displays the database panel:
    chrome_2019-04-15_05-49-16

  2. (Re)configure the database service to use native prepared statements:

database:
	dsn: ...
	user: ...
	password: ...
	options:
		PDO::ATTR_EMULATE_PREPARES: false
  1. Reload the page
  2. The database panel now displays an error:
    chrome_2019-04-15_05-50-00

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.