mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
BUG Fix major segfault on PDOConnector after any DDL
BUG Fix issue in PDOQuery::first() Refactor previewWrite and benchmarkQuery into SS_Database
This commit is contained in:
parent
d0bd5bc225
commit
ce3b5a5ace
@ -10,11 +10,20 @@ abstract class DBConnector {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List of operations to treat as write
|
* List of operations to treat as write
|
||||||
|
* Implicitly includes all ddl_operations
|
||||||
*
|
*
|
||||||
* @config
|
* @config
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private static $write_operations = array('insert', 'update', 'delete', 'replace', 'alter', 'drop');
|
private static $write_operations = array('insert', 'update', 'delete', 'replace');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of operations to treat as DDL
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $ddl_operations = array('alter', 'drop', 'create', 'truncate');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error handler for database errors.
|
* Error handler for database errors.
|
||||||
@ -27,6 +36,7 @@ abstract class DBConnector {
|
|||||||
* @param integer $errorLevel The level of the error to throw.
|
* @param integer $errorLevel The level of the error to throw.
|
||||||
* @param string $sql The SQL related to this query
|
* @param string $sql The SQL related to this query
|
||||||
* @param array $parameters Parameters passed to the query
|
* @param array $parameters Parameters passed to the query
|
||||||
|
* @throws SS_DatabaseException
|
||||||
*/
|
*/
|
||||||
protected function databaseError($msg, $errorLevel = E_USER_ERROR, $sql = null, $parameters = array()) {
|
protected function databaseError($msg, $errorLevel = E_USER_ERROR, $sql = null, $parameters = array()) {
|
||||||
// Prevent errors when error checking is set at zero level
|
// Prevent errors when error checking is set at zero level
|
||||||
@ -49,46 +59,57 @@ abstract class DBConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the query should be previewed, and thus interrupted silently.
|
* Determine if this SQL statement is a destructive operation (write or ddl)
|
||||||
* If so, this function also displays the query via the debuging system.
|
|
||||||
* Subclasess should respect the results of this call for each query, and not
|
|
||||||
* execute any queries that generate a true response.
|
|
||||||
*
|
*
|
||||||
* @param string $sql The query to be executed
|
* @param string $sql
|
||||||
* @return boolean Flag indicating that the query was previewed
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected function previewWrite($sql) {
|
public function isQueryMutable($sql) {
|
||||||
// Break if not requested
|
$operations = array_merge(
|
||||||
if (!isset($_REQUEST['previewwrite'])) return false;
|
Config::inst()->get(get_class($this), 'write_operations'),
|
||||||
|
Config::inst()->get(get_class($this), 'ddl_operations')
|
||||||
// Break if non-write operation
|
);
|
||||||
$operation = strtolower(substr($sql, 0, strpos($sql, ' ')));
|
return $this->isQueryType($sql, $operations);
|
||||||
$writeOperations = Config::inst()->get(get_class($this), 'write_operations');
|
|
||||||
if (!in_array($operation, $writeOperations)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// output preview message
|
|
||||||
Debug::message("Will execute: $sql");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the display and benchmarking of queries as they are being run
|
* Determine if this SQL statement is a DDL operation
|
||||||
*
|
*
|
||||||
* @param string $sql Query to run, and single parameter to callback
|
* @param string $sql
|
||||||
* @param callable $callback Callback to execute code
|
* @return bool
|
||||||
* @return mixed Result of query
|
|
||||||
*/
|
*/
|
||||||
protected function benchmarkQuery($sql, $callback) {
|
public function isQueryDDL($sql) {
|
||||||
if (isset($_REQUEST['showqueries']) && Director::isDev(true)) {
|
$operations = Config::inst()->get(get_class($this), 'ddl_operations');
|
||||||
$starttime = microtime(true);
|
return $this->isQueryType($sql, $operations);
|
||||||
$result = $callback($sql);
|
}
|
||||||
$endtime = round(microtime(true) - $starttime, 4);
|
|
||||||
Debug::message("\n$sql\n{$endtime}s\n", false);
|
/**
|
||||||
return $result;
|
* Determine if this SQL statement is a write operation
|
||||||
|
* (alters content but not structure)
|
||||||
|
*
|
||||||
|
* @param string $sql
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isQueryWrite($sql) {
|
||||||
|
$operations = Config::inst()->get(get_class($this), 'write_operations');
|
||||||
|
return $this->isQueryType($sql, $operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a query is of the given type
|
||||||
|
*
|
||||||
|
* @param string $sql Raw SQL
|
||||||
|
* @param string|array $type Type or list of types (first word in the query). Must be lowercase
|
||||||
|
*/
|
||||||
|
protected function isQueryType($sql, $type) {
|
||||||
|
if(!preg_match('/^(?<operation>\w+)\b/', $sql, $matches)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$operation = $matches['operation'];
|
||||||
|
if(is_array($type)) {
|
||||||
|
return in_array(strtolower($operation), $type);
|
||||||
} else {
|
} else {
|
||||||
return $callback($sql);
|
return strcasecmp($sql, $type) === 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,19 @@ abstract class SS_Database {
|
|||||||
* @return SS_Query
|
* @return SS_Query
|
||||||
*/
|
*/
|
||||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||||
return $this->connector->query($sql, $errorLevel);
|
// Check if we should only preview this query
|
||||||
|
if ($this->previewWrite($sql)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark query
|
||||||
|
$connector = $this->connector;
|
||||||
|
return $this->benchmarkQuery(
|
||||||
|
$sql,
|
||||||
|
function($sql) use($connector, $errorLevel) {
|
||||||
|
return $connector->query($sql, $errorLevel);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +121,62 @@ abstract class SS_Database {
|
|||||||
* @return SS_Query
|
* @return SS_Query
|
||||||
*/
|
*/
|
||||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||||
return $this->connector->preparedQuery($sql, $parameters, $errorLevel);
|
// Check if we should only preview this query
|
||||||
|
if ($this->previewWrite($sql)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark query
|
||||||
|
$connector = $this->connector;
|
||||||
|
return $this->benchmarkQuery(
|
||||||
|
$sql,
|
||||||
|
function($sql) use($connector, $parameters, $errorLevel) {
|
||||||
|
return $connector->preparedQuery($sql, $parameters, $errorLevel);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the query should be previewed, and thus interrupted silently.
|
||||||
|
* If so, this function also displays the query via the debuging system.
|
||||||
|
* Subclasess should respect the results of this call for each query, and not
|
||||||
|
* execute any queries that generate a true response.
|
||||||
|
*
|
||||||
|
* @param string $sql The query to be executed
|
||||||
|
* @return boolean Flag indicating that the query was previewed
|
||||||
|
*/
|
||||||
|
protected function previewWrite($sql) {
|
||||||
|
// Only preview if previewWrite is set, we are in dev mode, and
|
||||||
|
// the query is mutable
|
||||||
|
if (isset($_REQUEST['previewwrite'])
|
||||||
|
&& Director::isDev()
|
||||||
|
&& $this->connector->isQueryMutable($sql)
|
||||||
|
) {
|
||||||
|
// output preview message
|
||||||
|
Debug::message("Will execute: $sql");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the display and benchmarking of queries as they are being run
|
||||||
|
*
|
||||||
|
* @param string $sql Query to run, and single parameter to callback
|
||||||
|
* @param callable $callback Callback to execute code
|
||||||
|
* @return mixed Result of query
|
||||||
|
*/
|
||||||
|
protected function benchmarkQuery($sql, $callback) {
|
||||||
|
if (isset($_REQUEST['showqueries']) && Director::isDev()) {
|
||||||
|
$starttime = microtime(true);
|
||||||
|
$result = $callback($sql);
|
||||||
|
$endtime = round(microtime(true) - $starttime, 4);
|
||||||
|
Debug::message("\n$sql\n{$endtime}s\n", false);
|
||||||
|
return $result;
|
||||||
|
} else {
|
||||||
|
return $callback($sql);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,21 +104,21 @@ class MySQLiConnector extends DBConnector {
|
|||||||
return $this->dbConn->server_info;
|
return $this->dbConn->server_info;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function benchmarkQuery($sql, $callback) {
|
/**
|
||||||
|
* Invoked before any query is executed
|
||||||
|
*
|
||||||
|
* @param string $sql
|
||||||
|
*/
|
||||||
|
protected function beforeQuery($sql) {
|
||||||
// Clear the last statement
|
// Clear the last statement
|
||||||
$this->setLastStatement(null);
|
$this->setLastStatement(null);
|
||||||
return parent::benchmarkQuery($sql, $callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||||
// Check if we should only preview this query
|
$this->beforeQuery($sql);
|
||||||
if ($this->previewWrite($sql)) return;
|
|
||||||
|
|
||||||
// Benchmark query
|
// Benchmark query
|
||||||
$conn = $this->dbConn;
|
$handle = $this->dbConn->query($sql, MYSQLI_STORE_RESULT);
|
||||||
$handle = $this->benchmarkQuery($sql, function($sql) use($conn) {
|
|
||||||
return $conn->query($sql, MYSQLI_STORE_RESULT);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!$handle || $this->dbConn->error) {
|
if (!$handle || $this->dbConn->error) {
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||||
@ -208,23 +208,20 @@ class MySQLiConnector extends DBConnector {
|
|||||||
|
|
||||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||||
// Shortcut to basic query when not given parameters
|
// Shortcut to basic query when not given parameters
|
||||||
if(empty($parameters)) return $this->query($sql, $errorLevel);
|
if(empty($parameters)) {
|
||||||
|
return $this->query($sql, $errorLevel);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should only preview this query
|
$this->beforeQuery($sql);
|
||||||
if ($this->previewWrite($sql)) return;
|
|
||||||
|
|
||||||
// Type check, identify, and prepare parameters for passing to the statement bind function
|
// Type check, identify, and prepare parameters for passing to the statement bind function
|
||||||
$parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
|
$parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
|
||||||
|
|
||||||
// Benchmark query
|
// Benchmark query
|
||||||
$self = $this;
|
$statement = $this->prepareStatement($sql, $success);
|
||||||
$lastStatement = $this->benchmarkQuery($sql, function($sql) use($parsedParameters, $blobs, $self) {
|
if($success) {
|
||||||
|
|
||||||
$statement = $self->prepareStatement($sql, $success);
|
|
||||||
if(!$success) return null;
|
|
||||||
|
|
||||||
if($parsedParameters) {
|
if($parsedParameters) {
|
||||||
$self->bindParameters($statement, $parsedParameters);
|
$this->bindParameters($statement, $parsedParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind any blobs given
|
// Bind any blobs given
|
||||||
@ -234,18 +231,18 @@ class MySQLiConnector extends DBConnector {
|
|||||||
|
|
||||||
// Safely execute the statement
|
// Safely execute the statement
|
||||||
$statement->execute();
|
$statement->execute();
|
||||||
return $statement;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!$lastStatement || $lastStatement->error) {
|
if (!$success || $statement->error) {
|
||||||
$values = $this->parameterValues($parameters);
|
$values = $this->parameterValues($parameters);
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-select queries will have no result data
|
// Non-select queries will have no result data
|
||||||
if($lastStatement && ($metaData = $lastStatement->result_metadata())) {
|
$metaData = $statement->result_metadata();
|
||||||
return new MySQLStatement($lastStatement, $metaData);
|
if($metaData) {
|
||||||
|
return new MySQLStatement($statement, $metaData);
|
||||||
} else {
|
} else {
|
||||||
// Replicate normal behaviour of ->query() on non-select calls
|
// Replicate normal behaviour of ->query() on non-select calls
|
||||||
return new MySQLQuery($this, true);
|
return new MySQLQuery($this, true);
|
||||||
|
@ -30,11 +30,18 @@ class PDOConnector extends DBConnector {
|
|||||||
protected $databaseName = null;
|
protected $databaseName = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The most recent statement returned from PDODatabase->query
|
* If available, the row count of the last executed statement
|
||||||
*
|
*
|
||||||
* @var PDOStatement
|
* @var int|null
|
||||||
*/
|
*/
|
||||||
protected $lastStatement = null;
|
protected $rowCount = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error generated by the errorInfo() method of the last PDOStatement
|
||||||
|
*
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
protected $lastStatementError = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of prepared statements, cached by SQL string
|
* List of prepared statements, cached by SQL string
|
||||||
@ -58,13 +65,22 @@ class PDOConnector extends DBConnector {
|
|||||||
* @return PDOStatement
|
* @return PDOStatement
|
||||||
*/
|
*/
|
||||||
public function getOrPrepareStatement($sql) {
|
public function getOrPrepareStatement($sql) {
|
||||||
if(empty($this->cachedStatements[$sql])) {
|
// Return cached statements
|
||||||
$this->cachedStatements[$sql] = $this->pdoConnection->prepare(
|
if(isset($this->cachedStatements[$sql])) {
|
||||||
|
return $this->cachedStatements[$sql];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new statement
|
||||||
|
$statement = $this->pdoConnection->prepare(
|
||||||
$sql,
|
$sql,
|
||||||
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
|
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Only cache select statements
|
||||||
|
if(preg_match('/^(\s*)select\b/i', $sql)) {
|
||||||
|
$this->cachedStatements[$sql] = $statement;
|
||||||
}
|
}
|
||||||
return $this->cachedStatements[$sql];
|
return $statement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,52 +184,52 @@ class PDOConnector extends DBConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a query that doesn't return a resultset
|
* Invoked before any query is executed
|
||||||
*
|
*
|
||||||
* @param string $sql
|
* @param string $sql
|
||||||
|
*/
|
||||||
|
protected function beforeQuery($sql) {
|
||||||
|
// Reset state
|
||||||
|
$this->rowCount = 0;
|
||||||
|
$this->lastStatementError = null;
|
||||||
|
|
||||||
|
// Flush if necessary
|
||||||
|
if($this->isQueryDDL($sql)) {
|
||||||
|
$this->flushStatements();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a query that doesn't return a resultset
|
||||||
|
*
|
||||||
* @param string $sql The SQL query to execute
|
* @param string $sql The SQL query to execute
|
||||||
* @param integer $errorLevel For errors to this query, raise PHP errors
|
* @param integer $errorLevel For errors to this query, raise PHP errors
|
||||||
* using this error level.
|
* using this error level.
|
||||||
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function exec($sql, $errorLevel = E_USER_ERROR) {
|
public function exec($sql, $errorLevel = E_USER_ERROR) {
|
||||||
// Check if we should only preview this query
|
$this->beforeQuery($sql);
|
||||||
if ($this->previewWrite($sql)) return;
|
|
||||||
|
|
||||||
// Reset last statement to prevent interference in case of error
|
// Directly exec this query
|
||||||
$this->lastStatement = null;
|
$result = $this->pdoConnection->exec($sql);
|
||||||
|
|
||||||
// Benchmark query
|
|
||||||
$pdo = $this->pdoConnection;
|
|
||||||
$result = $this->benchmarkQuery($sql, function($sql) use($pdo) {
|
|
||||||
return $pdo->exec($sql);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for errors
|
// Check for errors
|
||||||
if ($result === false) {
|
if ($result !== false) {
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
return $this->rowCount = $result;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||||
// Check if we should only preview this query
|
$this->beforeQuery($sql);
|
||||||
if ($this->previewWrite($sql)) return;
|
|
||||||
|
|
||||||
// Benchmark query
|
// Directly query against connection
|
||||||
$pdo = $this->pdoConnection;
|
$statement = $this->pdoConnection->query($sql);
|
||||||
$this->lastStatement = $this->benchmarkQuery($sql, function($sql) use($pdo) {
|
|
||||||
return $pdo->query($sql);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for errors
|
// Generate results
|
||||||
if (!$this->lastStatement || $this->hasError($this->lastStatement)) {
|
return $this->prepareResults($statement, $errorLevel, $sql);
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PDOQuery($this->lastStatement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,33 +288,54 @@ class PDOConnector extends DBConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||||
// Check if we should only preview this query
|
$this->beforeQuery($sql);
|
||||||
if ($this->previewWrite($sql)) return;
|
|
||||||
|
|
||||||
// Benchmark query
|
|
||||||
$self = $this;
|
|
||||||
$this->lastStatement = $this->benchmarkQuery($sql, function($sql) use($parameters, $self) {
|
|
||||||
|
|
||||||
// Prepare statement
|
// Prepare statement
|
||||||
$statement = $self->getOrPrepareStatement($sql);
|
$statement = $this->getOrPrepareStatement($sql);
|
||||||
if(!$statement) return null;
|
|
||||||
|
|
||||||
// Inject parameters
|
// Bind and invoke statement safely
|
||||||
$self->bindParameters($statement, $parameters);
|
if($statement) {
|
||||||
|
$this->bindParameters($statement, $parameters);
|
||||||
// Safely execute the statement
|
|
||||||
$statement->execute($parameters);
|
$statement->execute($parameters);
|
||||||
return $statement;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
if (!$this->lastStatement || $this->hasError($this->lastStatement)) {
|
|
||||||
$values = $this->parameterValues($parameters);
|
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PDOQuery($this->lastStatement);
|
// Generate results
|
||||||
|
return $this->prepareResults($statement, $errorLevel, $sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a PDOStatement that has just been executed, generate results
|
||||||
|
* and report any errors
|
||||||
|
*
|
||||||
|
* @param PDOStatement $statement
|
||||||
|
* @param int $errorLevel
|
||||||
|
* @param string $sql
|
||||||
|
* @param array $parameters
|
||||||
|
* @return \PDOQuery
|
||||||
|
*/
|
||||||
|
protected function prepareResults($statement, $errorLevel, $sql, $parameters = array()) {
|
||||||
|
|
||||||
|
// Record row-count and errors of last statement
|
||||||
|
if($this->hasError($statement)) {
|
||||||
|
$this->lastStatementError = $statement->errorInfo();
|
||||||
|
} elseif($statement) {
|
||||||
|
// Count and return results
|
||||||
|
$this->rowCount = $statement->rowCount();
|
||||||
|
return new PDOQuery($statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure statement is closed
|
||||||
|
if($statement) {
|
||||||
|
$statement->closeCursor();
|
||||||
|
unset($statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report any errors
|
||||||
|
if($parameters) {
|
||||||
|
$parameters = $this->parameterValues($parameters);
|
||||||
|
}
|
||||||
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -309,11 +346,15 @@ class PDOConnector extends DBConnector {
|
|||||||
*/
|
*/
|
||||||
protected function hasError($resource) {
|
protected function hasError($resource) {
|
||||||
// No error if no resource
|
// No error if no resource
|
||||||
if(empty($resource)) return false;
|
if(empty($resource)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If the error code is empty the statement / connection has not been run yet
|
// If the error code is empty the statement / connection has not been run yet
|
||||||
$code = $resource->errorCode();
|
$code = $resource->errorCode();
|
||||||
if(empty($code)) return false;
|
if(empty($code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip 'ok' and undefined 'warning' types.
|
// Skip 'ok' and undefined 'warning' types.
|
||||||
// @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
|
// @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
|
||||||
@ -321,12 +362,13 @@ class PDOConnector extends DBConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getLastError() {
|
public function getLastError() {
|
||||||
if ($this->hasError($this->lastStatement)) {
|
$error = null;
|
||||||
$error = $this->lastStatement->errorInfo();
|
if ($this->lastStatementError) {
|
||||||
|
$error = $this->lastStatementError;
|
||||||
} elseif($this->hasError($this->pdoConnection)) {
|
} elseif($this->hasError($this->pdoConnection)) {
|
||||||
$error = $this->pdoConnection->errorInfo();
|
$error = $this->pdoConnection->errorInfo();
|
||||||
}
|
}
|
||||||
if (isset($error)) {
|
if ($error) {
|
||||||
return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
|
return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,8 +378,7 @@ class PDOConnector extends DBConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function affectedRows() {
|
public function affectedRows() {
|
||||||
if (empty($this->lastStatement)) return 0;
|
return $this->rowCount;
|
||||||
return $this->lastStatement->rowCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function selectDatabase($name) {
|
public function selectDatabase($name) {
|
||||||
|
@ -146,6 +146,7 @@ abstract class SS_Query implements Iterator {
|
|||||||
public function rewind() {
|
public function rewind() {
|
||||||
if ($this->queryHasBegun && $this->numRecords() > 0) {
|
if ($this->queryHasBegun && $this->numRecords() > 0) {
|
||||||
$this->queryHasBegun = false;
|
$this->queryHasBegun = false;
|
||||||
|
$this->currentRecord = null;
|
||||||
return $this->seek(0);
|
return $this->seek(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
113
tests/model/PDODatabaseTest.php
Normal file
113
tests/model/PDODatabaseTest.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage testing
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PDODatabaseTest extends SapphireTest {
|
||||||
|
|
||||||
|
protected static $fixture_file = 'MySQLDatabaseTest.yml';
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'MySQLDatabaseTest_Data'
|
||||||
|
);
|
||||||
|
|
||||||
|
public function testPreparedStatements() {
|
||||||
|
if(!(DB::get_connector() instanceof PDOConnector)) {
|
||||||
|
$this->markTestSkipped('This test requires the current DB connector is PDO');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test preparation of equivalent statemetns
|
||||||
|
$result1 = DB::get_connector()->preparedQuery(
|
||||||
|
'SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" WHERE "Sort" > ? ORDER BY "Sort"',
|
||||||
|
array(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$result2 = DB::get_connector()->preparedQuery(
|
||||||
|
'SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" WHERE "Sort" > ? ORDER BY "Sort"',
|
||||||
|
array(2)
|
||||||
|
);
|
||||||
|
$this->assertInstanceOf('PDOQuery', $result1);
|
||||||
|
$this->assertInstanceOf('PDOQuery', $result2);
|
||||||
|
|
||||||
|
// Also select non-prepared statement
|
||||||
|
$result3 = DB::get_connector()->query('SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" ORDER BY "Sort"');
|
||||||
|
$this->assertInstanceOf('PDOQuery', $result3);
|
||||||
|
|
||||||
|
// Iterating one level should not buffer, but return the right result
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result1->next()
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 2,
|
||||||
|
'Title' => 'Second Item'
|
||||||
|
),
|
||||||
|
$result1->next()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test first
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result1->first()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test seek
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 2,
|
||||||
|
'Title' => 'Second Item'
|
||||||
|
),
|
||||||
|
$result1->seek(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test count
|
||||||
|
$this->assertEquals(4, $result1->numRecords());
|
||||||
|
|
||||||
|
// Test second statement
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 3,
|
||||||
|
'Title' => 'Third Item'
|
||||||
|
),
|
||||||
|
$result2->next()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test non-prepared query
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result3->next()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAffectedRows() {
|
||||||
|
if(!(DB::get_connector() instanceof PDOConnector)) {
|
||||||
|
$this->markTestSkipped('This test requires the current DB connector is PDO');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new SQLUpdate('MySQLDatabaseTest_Data');
|
||||||
|
$query->setAssignments(array('Title' => 'New Title'));
|
||||||
|
|
||||||
|
// Test update which affects no rows
|
||||||
|
$query->setWhere(array('Title' => 'Bob'));
|
||||||
|
$result = $query->execute();
|
||||||
|
$this->assertInstanceOf('PDOQuery', $result);
|
||||||
|
$this->assertEquals(0, DB::affected_rows());
|
||||||
|
|
||||||
|
// Test update which affects some rows
|
||||||
|
$query->setWhere(array('Title' => 'First Item'));
|
||||||
|
$result = $query->execute();
|
||||||
|
$this->assertInstanceOf('PDOQuery', $result);
|
||||||
|
$this->assertEquals(1, DB::affected_rows());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user