mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
FIX: Fix PDO cached statement column coercion
NEW: Add PDOStatementHandle class that is now what PDOQuery expects
This commit is contained in:
parent
2625cea5e3
commit
c9c7c0c825
@ -98,7 +98,7 @@ class PDOConnector extends DBConnector implements TransactionManager
|
|||||||
* one exists for the given query
|
* one exists for the given query
|
||||||
*
|
*
|
||||||
* @param string $sql
|
* @param string $sql
|
||||||
* @return PDOStatement
|
* @return PDOStatementHandle|false
|
||||||
*/
|
*/
|
||||||
public function getOrPrepareStatement($sql)
|
public function getOrPrepareStatement($sql)
|
||||||
{
|
{
|
||||||
@ -113,11 +113,14 @@ class PDOConnector extends DBConnector implements TransactionManager
|
|||||||
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
|
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wrap in a PDOStatementHandle, to cache column metadata
|
||||||
|
$statementHandle = ($statement === false) ? false : new PDOStatementHandle($statement);
|
||||||
|
|
||||||
// Only cache select statements
|
// Only cache select statements
|
||||||
if (preg_match('/^(\s*)select\b/i', $sql)) {
|
if (preg_match('/^(\s*)select\b/i', $sql)) {
|
||||||
$this->cachedStatements[$sql] = $statement;
|
$this->cachedStatements[$sql] = $statementHandle;
|
||||||
}
|
}
|
||||||
return $statement;
|
return $statementHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,7 +217,10 @@ class PDOConnector extends DBConnector implements TransactionManager
|
|||||||
$options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
|
$options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca'];
|
||||||
}
|
}
|
||||||
// use default cipher if not provided
|
// use default cipher if not provided
|
||||||
$options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default');
|
$options[PDO::MYSQL_ATTR_SSL_CIPHER] =
|
||||||
|
array_key_exists('ssl_cipher', $parameters) ?
|
||||||
|
$parameters['ssl_cipher'] :
|
||||||
|
self::config()->get('ssl_cipher_default');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (static::config()->get('legacy_types')) {
|
if (static::config()->get('legacy_types')) {
|
||||||
@ -328,7 +334,11 @@ class PDOConnector extends DBConnector implements TransactionManager
|
|||||||
$statement = $this->pdoConnection->query($sql);
|
$statement = $this->pdoConnection->query($sql);
|
||||||
|
|
||||||
// Generate results
|
// Generate results
|
||||||
return $this->prepareResults($statement, $errorLevel, $sql);
|
if ($statement === false) {
|
||||||
|
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||||
|
} else {
|
||||||
|
return $this->prepareResults(new PDOStatementHandle($statement), $errorLevel, $sql);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -395,54 +405,51 @@ class PDOConnector extends DBConnector implements TransactionManager
|
|||||||
{
|
{
|
||||||
$this->beforeQuery($sql);
|
$this->beforeQuery($sql);
|
||||||
|
|
||||||
// Prepare statement
|
// Fetch cached statement, or create it
|
||||||
$statement = $this->getOrPrepareStatement($sql);
|
$statementHandle = $this->getOrPrepareStatement($sql);
|
||||||
|
|
||||||
// Bind and invoke statement safely
|
// Error handling
|
||||||
if ($statement) {
|
if ($statementHandle === false) {
|
||||||
$this->bindParameters($statement, $parameters);
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters));
|
||||||
$statement->execute($parameters);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
$this->bindParameters($statementHandle->getPDOStatement(), $parameters);
|
||||||
|
$statementHandle->execute($parameters);
|
||||||
|
|
||||||
// Generate results
|
// Generate results
|
||||||
return $this->prepareResults($statement, $errorLevel, $sql);
|
return $this->prepareResults($statementHandle, $errorLevel, $sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a PDOStatement that has just been executed, generate results
|
* Given a PDOStatement that has just been executed, generate results
|
||||||
* and report any errors
|
* and report any errors
|
||||||
*
|
*
|
||||||
* @param PDOStatement $statement
|
* @param PDOStatementHandle $statement
|
||||||
* @param int $errorLevel
|
* @param int $errorLevel
|
||||||
* @param string $sql
|
* @param string $sql
|
||||||
* @param array $parameters
|
* @param array $parameters
|
||||||
* @return PDOQuery
|
* @return PDOQuery
|
||||||
*/
|
*/
|
||||||
protected function prepareResults($statement, $errorLevel, $sql, $parameters = array())
|
protected function prepareResults(PDOStatementHandle $statement, $errorLevel, $sql, $parameters = array())
|
||||||
{
|
{
|
||||||
|
|
||||||
// Record row-count and errors of last statement
|
// Catch error
|
||||||
if ($this->hasError($statement)) {
|
if ($this->hasError($statement)) {
|
||||||
$this->lastStatementError = $statement->errorInfo();
|
$this->lastStatementError = $statement->errorInfo();
|
||||||
} elseif ($statement) {
|
$statement->closeCursor();
|
||||||
|
|
||||||
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters));
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Count and return results
|
// Count and return results
|
||||||
$this->rowCount = $statement->rowCount();
|
$this->rowCount = $statement->rowCount();
|
||||||
return new PDOQuery($statement);
|
return new PDOQuery($statement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure statement is closed
|
|
||||||
if ($statement) {
|
|
||||||
$statement->closeCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report any errors
|
|
||||||
if ($parameters) {
|
|
||||||
$parameters = $this->parameterValues($parameters);
|
|
||||||
}
|
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a resource has an attached error
|
* Determine if a resource has an attached error
|
||||||
*
|
*
|
||||||
|
@ -2,82 +2,30 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\Connect;
|
namespace SilverStripe\ORM\Connect;
|
||||||
|
|
||||||
use PDOStatement;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A result-set from a PDO database.
|
* A result-set from a PDO database.
|
||||||
*/
|
*/
|
||||||
class PDOQuery extends Query
|
class PDOQuery extends Query
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The internal MySQL handle that points to the result set.
|
* @var array
|
||||||
* @var PDOStatement
|
|
||||||
*/
|
*/
|
||||||
protected $statement = null;
|
|
||||||
|
|
||||||
protected $results = null;
|
protected $results = null;
|
||||||
|
|
||||||
protected static $type_mapping = [
|
|
||||||
// PGSQL
|
|
||||||
'float8' => 'float',
|
|
||||||
'float16' => 'float',
|
|
||||||
'numeric' => 'float',
|
|
||||||
|
|
||||||
// MySQL
|
|
||||||
'NEWDECIMAL' => 'float',
|
|
||||||
|
|
||||||
// SQlite
|
|
||||||
'integer' => 'int',
|
|
||||||
'double' => 'float',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
||||||
* @param PDOStatement $statement The internal PDOStatement containing the results
|
* @param PDOStatement $statement The internal PDOStatement containing the results
|
||||||
*/
|
*/
|
||||||
public function __construct(PDOStatement $statement)
|
public function __construct(PDOStatementHandle $statement)
|
||||||
{
|
{
|
||||||
$this->statement = $statement;
|
|
||||||
// Since no more than one PDOStatement for any one connection can be safely
|
// Since no more than one PDOStatement for any one connection can be safely
|
||||||
// traversed, each statement simply requests all rows at once for safety.
|
// traversed, each statement simply requests all rows at once for safety.
|
||||||
// This could be re-engineered to call fetchAll on an as-needed basis
|
// This could be re-engineered to call fetchAll on an as-needed basis
|
||||||
|
|
||||||
$this->results = $this->typeCorrectedFetchAll($statement);
|
$this->results = $statement->typeCorrectedFetchAll();
|
||||||
|
|
||||||
$statement->closeCursor();
|
$statement->closeCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a record form the statement with its type data corrected
|
|
||||||
* Returns data as an array of maps
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function typeCorrectedFetchAll($statement)
|
|
||||||
{
|
|
||||||
$columnCount = $statement->columnCount();
|
|
||||||
$columnMeta = [];
|
|
||||||
for ($i = 0; $i<$columnCount; $i++) {
|
|
||||||
$columnMeta[$i] = $statement->getColumnMeta($i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-map fetched data using columnMeta
|
|
||||||
return array_map(
|
|
||||||
function ($rowArray) use ($columnMeta) {
|
|
||||||
$row = [];
|
|
||||||
foreach ($columnMeta as $i => $meta) {
|
|
||||||
// Coerce any column types that aren't correctly retrieved from the database
|
|
||||||
if (isset($meta['native_type']) && isset(self::$type_mapping[$meta['native_type']])) {
|
|
||||||
settype($rowArray[$i], self::$type_mapping[$meta['native_type']]);
|
|
||||||
}
|
|
||||||
$row[$meta['name']] = $rowArray[$i];
|
|
||||||
}
|
|
||||||
return $row;
|
|
||||||
},
|
|
||||||
$statement->fetchAll(PDO::FETCH_NUM)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function seek($row)
|
public function seek($row)
|
||||||
{
|
{
|
||||||
$this->rowNum = $row - 1;
|
$this->rowNum = $row - 1;
|
||||||
|
154
src/ORM/Connect/PDOStatementHandle.php
Normal file
154
src/ORM/Connect/PDOStatementHandle.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Connect;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOStatement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handle to a PDOStatement, with cached column metadata, and type conversion
|
||||||
|
*
|
||||||
|
* Column metadata can't be fetched from a native PDOStatement after multiple calls in some DB backends,
|
||||||
|
* so we wrap in this handle object, which also takes care of tidying up content types to keep in line
|
||||||
|
* with the SilverStripe 4.4+ type expectations.
|
||||||
|
*/
|
||||||
|
class PDOStatementHandle
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The statement to provide a handle to
|
||||||
|
*
|
||||||
|
* @var PDOStatement
|
||||||
|
*/
|
||||||
|
private $statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached column metadata
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $columnMeta = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new handle.
|
||||||
|
*
|
||||||
|
* @param $statement The statement to provide a handle to
|
||||||
|
*/
|
||||||
|
public function __construct(PDOStatement $statement)
|
||||||
|
{
|
||||||
|
$this->statement = $statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of PDO-reported "native types" to PHP types
|
||||||
|
*/
|
||||||
|
protected static $type_mapping = [
|
||||||
|
// PGSQL
|
||||||
|
'float8' => 'float',
|
||||||
|
'float16' => 'float',
|
||||||
|
'numeric' => 'float',
|
||||||
|
|
||||||
|
// MySQL
|
||||||
|
'NEWDECIMAL' => 'float',
|
||||||
|
|
||||||
|
// SQlite
|
||||||
|
'integer' => 'int',
|
||||||
|
'double' => 'float',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a record form the statement with its type data corrected
|
||||||
|
* Returns data as an array of maps
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function typeCorrectedFetchAll()
|
||||||
|
{
|
||||||
|
if ($this->columnMeta === null) {
|
||||||
|
$columnCount = $this->statement->columnCount();
|
||||||
|
$this->columnMeta = [];
|
||||||
|
for ($i = 0; $i<$columnCount; $i++) {
|
||||||
|
$this->columnMeta[$i] = $this->statement->getColumnMeta($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-map fetched data using columnMeta
|
||||||
|
return array_map(
|
||||||
|
function ($rowArray) {
|
||||||
|
$row = [];
|
||||||
|
foreach ($this->columnMeta as $i => $meta) {
|
||||||
|
// Coerce any column types that aren't correctly retrieved from the database
|
||||||
|
if (isset($meta['native_type']) && isset(self::$type_mapping[$meta['native_type']])) {
|
||||||
|
settype($rowArray[$i], self::$type_mapping[$meta['native_type']]);
|
||||||
|
}
|
||||||
|
$row[$meta['name']] = $rowArray[$i];
|
||||||
|
}
|
||||||
|
return $row;
|
||||||
|
},
|
||||||
|
$this->statement->fetchAll(PDO::FETCH_NUM)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the cursor, enabling the statement to be executed again (PDOStatement::closeCursor)
|
||||||
|
*
|
||||||
|
* @return bool Returns true on success
|
||||||
|
*/
|
||||||
|
public function closeCursor()
|
||||||
|
{
|
||||||
|
return $this->statement->closeCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the SQLSTATE associated with the last operation on the statement handle
|
||||||
|
* (PDOStatement::errorCode)
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function errorCode()
|
||||||
|
{
|
||||||
|
return $this->statement->errorCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch extended error information associated with the last operation on the statement handle
|
||||||
|
* (PDOStatement::errorInfo)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function errorInfo()
|
||||||
|
{
|
||||||
|
return $this->statement->errorInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of rows affected by the last SQL statement (PDOStatement::rowCount)
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function rowCount()
|
||||||
|
{
|
||||||
|
return $this->statement->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a prepared statement (PDOStatement::execute)
|
||||||
|
*
|
||||||
|
* @param $parameters An array of values with as many elements as there are bound parameters in the SQL statement
|
||||||
|
* being executed
|
||||||
|
* @return bool Returns true on success
|
||||||
|
*/
|
||||||
|
public function execute(array $parameters)
|
||||||
|
{
|
||||||
|
return $this->statement->execute($parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the PDOStatement that this object provides a handle to
|
||||||
|
*
|
||||||
|
* @return PDOStatement
|
||||||
|
*/
|
||||||
|
public function getPDOStatement()
|
||||||
|
{
|
||||||
|
return $this->statement;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user