Skip to content

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

@spaze

Description

@spaze

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions