Page MenuHomePhabricator

DatabaseMSPDO.php

Authored By
bzimport
Nov 21 2014, 9:38 PM
Size
46 KB
Referenced Files
None
Subscribers
None

DatabaseMSPDO.php

<?php
/**
* @defgroup Database DatabaseMSPDO
*
* @file
* @ingroup Database
* This file deals with Microsoft SQL interface functions
* and query specifics/optimisations using PDO
* @package MediaWiki
*/
/******************************************************************************/
class PDOdbMetaData {
public $name = '';
public $table = '';
public $def = '';
public $max_length = 1;
public $not_null = 1;
public $primary_key = 0;
public $unique_key = 0;
public $multiple_key = 1;
public $blob = 0;
public $type = 0;
public $unsigned = 0;
public $zerofill = 0;
public $non_unique = 1;
}
/**
* Specialization of the Database abstraction object
* @package MediaWiki
*/
class DatabasePDO extends Database {
#------------------------------------------------------------------------------
# Variables
#------------------------------------------------------------------------------
var $replaceQuote = "\\'"; /// string to use to replace quotes
#------------------------------------------------------------------------------
# Accessors
#------------------------------------------------------------------------------
# These optionally set a variable and return the previous state
/**
* Returns true if this database uses timestamps rather than integers
*/
function realTimestamps() {
return true;
}
#------------------------------------------------------------------------------
# Other functions
#------------------------------------------------------------------------------
/**
* Constructor.
* @param $server String: database server host
* @param $user String: database user name
* @param $password String: database user password
* @param $dbName String: database name
* @param $failFunction
* @param $flags
* @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
*/
function __construct( $server = false, $user = false, $password = false, $dbName = false,
$failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
if ($server == "localhost")
$server = "(local)";
parent::__construct($server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix);
$this->limitOffset = 0;
}
/**
* Same as new Database( ... ), kept for backward compatibility
* @param $server String: database server host
* @param $user String: database user name
* @param $password String: database user password
* @param $dbName String: database name
* @param failFunction
* @param $flags
*/
static function newFromParams( $server, $user, $password, $dbName,
$failFunction = false, $flags = 0 )
{
if ($server == "localhost")
$server = "(local)";
return new DatabasePDO( $server, $user, $password, $dbName, $failFunction, $flags );
}
/**
* Usually aborts on failure
* If the failFunction is set to a non-zero integer, returns success
* @param $server String: database server host
* @param $user String: database user name
* @param $password String: database user password
* @param $dbName String: database name
*/
function open( $server, $user, $password, $dbName ) {
global $wguname;
wfProfileIn( __METHOD__ );
if ($server == "localhost")
$server = "(local)";
$this->close();
$this->mServer = $server;
$this->mUser = $user;
$this->mPassword = $password;
$this->mDBname = $dbName;
$success = false;
wfDebug( "Opening db $server\n" );
wfProfileIn("dbconnect-$server");
$this->mConn = false;
$max = 3;
$dsn = "sqlsrv:Server=$server;Database=$dbName";
for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
if ( $i > 1 ) {
usleep( 1000 );
}
if ( $this->mFlags & DBO_PERSISTENT ) {
try {
$this->mConn = new PDO($dsn, $user, $password);
$this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
//$this->mConn->setAttribute( PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_SYSTEM );
$this->mConn->setAttribute( PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8 );
$this->mConn->setAttribute( PDO::ATTR_CASE, PDO::CASE_LOWER);
} catch (exception $pe) {
throw new DBConnectionError($this, $pe->getMessage());
}
} else {
# Create a new connection...
try {
$this->mConn = new PDO($dsn, $user, $password);
$this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
} catch (exception $pe) {
throw new DBConnectionError($this, $pe->getMessage());
}
}
if ($this->mConn === false) {
$iplus = $i + 1;
wfLogDBError("Connect loop error $iplus of $max ($server): " . " - " . "\n");
}
}
$this->mConn->exec("SET NOCOUNT OFF");
$this->mConn->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_BOTH);
wfProfileOut("dbconnect-$server");
$success = (bool)$this->mConn;
if ( $success ) {
// Setting the character set is handled differently from MySQL
} else {
$this->reportConnectionError();
}
$this->mOpened = $success;
wfProfileOut( __METHOD__ );
return $success;
}
/**@}}*/
public function resetConnection() {
return $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname);
}
/**
* Closes a database connection.
* if it is open : commits any open transactions
*
* @return Bool operation success. true if already closed.
*/
function close()
{
wfDebug( "Closing database\n");
$this->mOpened = false;
if ( $this->mConn ) {
if ( $this->trxLevel() ) {
$this->immediateCommit();
}
$this->mConn = null;
return true;
} else {
return true;
}
}
/**
* Usually aborts on failure
* If errors are explicitly ignored, returns success
*/
function query( $sql, $fname = '', $tempIgnore = false ) {
global $wgProfiling;
if ( !strncmp( "LOCK", $sql, 4) )
return;
if ( !strncmp( "UNLOCK", $sql, 6) )
return;
$limitLoc = strrpos($sql, "LIMIT");
// TODO: Clean up this LIMIT --> SELECT TOP nonsense
if ( $limitLoc != FALSE && $limitLoc > strlen( $sql ) - 15 ) {
$foundLimit = preg_match( '/LIMIT (\d*)/', $sql, $m );
if ( $foundLimit ) {
$limitNum = $m[1]; //substr( $sql, $limitLoc + 5 );
$sql = preg_replace( '/LIMIT \d*/', '', $sql);
}
//$numLen = strlen($limitNum);
$topLoc = strpos($sql, "SELECT TOP");
if ( $topLoc === FALSE ) {
$selectPos = strpos($sql, "SELECT");
if ( $selectPos === FALSE )
$endReplace = 0;
else if ( $selectPos == 0)
$endReplace = 6;
else if ( $selectPos == 1)
$endReplace = 0;
$sql = substr_replace( $sql, "SELECT TOP $limitNum ", 0, $endReplace );
}
}
if ( $wgProfiling ) {
# generalizeSQL will probably cut down the query to reasonable
# logging size most of the time. The substr is really just a sanity check.
# Who's been wasting my precious column space? -- TS
#$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
if ( is_null( $this->getLBInfo( 'master' ) ) ) {
$queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
$totalProf = 'Database::query';
} else {
$queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
$totalProf = 'Database::query-master';
}
wfProfileIn( $totalProf );
wfProfileIn( $queryProf );
}
$this->mLastQuery = $sql;
# Add a comment for easy SHOW PROCESSLIST interpretation
if ( $fname ) {
$commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
} else {
$commentedSql = $sql;
}
# If DBO_TRX is set, start a transaction
if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
$sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK'
) {
$this->begin();
}
if ( $this->debug() ) {
$sqlx = substr( $commentedSql, 0, 500 );
$sqlx = strtr( $sqlx, "\t\n", ' ' );
wfDebug( "SQL: $sqlx\n" );
}
# Do the query and handle errors
$ret = $this->doQuery( $commentedSql );
# Try reconnecting if the connection was lost
if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
wfDebug( "Reconnected\n" );
$ret = $this->doQuery( $commentedSql );
} else {
wfDebug( "Failed\n" );
}
}
if ( false === $ret ) {
$this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
}
if ( isset( $wgProfiler ) ) {
wfProfileOut( $queryProf );
wfProfileOut( $totalProf );
}
return $this->resultObject( $ret );
}
/**
* The DBMS-dependent part of query()
* @param $sql String: SQL query.
* @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
* @private
*/
function doQuery( $sql ) {
$ret = false;
try {
$pdoStmt = $this->mConn->prepare( $sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL) );
$this->lastPDOStatement = $pdoStmt;
$ok = $pdoStmt->execute();
if ( $ok ) {
$ret = $pdoStmt;
}
} catch (exception $e) {
//adodb_backtrace($e->gettrace());
}
return $ret;
}
/**#@+
* @param mixed $res A SQL result
*/
/**
* Free a result object
*/
function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
$res->closeCursor();
}
/**
* Fetch the next row from the given result object, in object form.
* Fields can be retrieved with $row->fieldname, with fields acting like
* member variables.
*
* @param $res SQL result object as returned from Database::query(), etc.
* @return MySQL row object
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
if (! $res )
return false;
if ($this->limitOffset != 0) {
$row = $res->fetch(PDO::FETCH_OBJ, PDO::FETCH_ORI_ABS, $this->limitOffset);
$this->limitOffset = 0;
} else {
$row = $res->fetchObject();
}
if( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
}
return $row;
}
function fetchNextObject( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
return $this->fetchObject( $res );
}
/**
* Fetch the next row from the given result object, in associative array
* form. Fields are retrieved with $row['fieldname'].
*
* @param $res SQL result object as returned from Database::query(), etc.
* @return MySQL row object
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
//$res->setFetchMode(PDO::FETCH_ASSOC);
if ($this->limitOffset != 0) {
$res->Move($this->limitOffset);
$this->limitOffset = 0;
}
$row = $res->fetch(PDO::FETCH_BOTH);
if ( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
}
return $row;
}
/**
* Get the number of rows in a result object
* Lies about the number of row if there was an offset!
*/
function numRows( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
$retVal = $res->rowCount();
if ( $this->limitOffset != 0) {
return $retVal - $this->limitOffset;
} else {
return $retVal;
}
}
/**
* Get the number of fields in a result object
* See documentation for mssql_num_fields()
*/
function numFields( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
return $res->columnCount();
}
/**
* Get a field name in a result object
* See documentation for mssql_field_name():
* http://www.php.net/mssql_field_name
*/
function fieldName( $res, $n ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
$fldObj = $res->getColumnMeta($n);
return $fldObj['name'];
}
/**
* Get the inserted value of an auto-increment row
*
* The value inserted should be fetched from nextSequenceValue()
*
* Example:
* $id = $dbw->nextSequenceValue('page_page_id_seq');
* $dbw->insert('page',array('page_id' => $id));
* $id = $dbw->insertId();
*/
function insertId() {
return $this->mConn->lastInsertId();
}
/**
* Change the position of the cursor in a result object
* See mysql_data_seek()
* @param $res Mixed: A SQL result
* @param $row Mixed: Either MySQL row or ResultWrapper
*/
function dataSeek( $res, $row ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
if ($row == 0) {
$res->execute();
} else {
$res->fetch( PDO::FETCH_BOTH, PDO::FETCH_ORI_ABS, $row - 1);
return $res;
}
return $res;
}
/**
* Get the last error number
* See mssql_errno()
*/
function lastErrno() {
if ( $this->mConn ) {
$eInfo = $this->mConn->errorInfo();
return (int) $eInfo[0];
} else {
return -1;
}
}
/**
* Get a description of the last error
* See mssql_error() for more details
*/
function lastError() {
if ( $this->mConn ) {
# Even if it's non-zero, it can still be invalid
wfSuppressWarnings();
$error = $this->mConn->errorInfo();
wfRestoreWarnings();
} else {
$error = "No Connection";
}
if( isset($error[0]) && $error[0] != 0 ) {
$error = $error[2] . ' (' . $this->mServer . ')';
return $error;
}
return 0;
}
/**
* Get the number of rows affected by the last write query
* See mssql_affected_rows() for more details
*/
function affectedRows() {
return $this->lastPDOStatement->rowCount();
}
/**#@-*/ // end of template : @param $result
/**
* Simple SELECT wrapper, returns a single field, input must be encoded
* Usually aborts on failure
* If errors are explicitly ignored, returns FALSE on failure
*/
function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
if ( !is_array( $options ) ) {
$options = array( $options );
}
#$options['LIMIT'] = 1;
$res = $this->select( $table, $var, $cond, $fname, $options );
if ( $res === false || !$this->numRows( $res ) ) {
return false;
}
$row = $this->fetchRow( $res );
if ( $row !== false ) {
$this->freeResult( $res );
return $row[0];
} else {
return false;
}
}
/**
* Returns an optional USE INDEX clause to go after the table, and a
* string to go at the end of the query
*
* @private
*
* @param array $options an associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
*/
function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = '';
$noKeyOptions = array();
foreach ( $options as $key => $option ) {
if ( is_numeric( $key ) ) {
$noKeyOptions[$option] = true;
}
}
if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
//if (isset($options['LIMIT'])) {
// $tailOpts .= $this->limitResult('', $options['LIMIT'],
// isset($options['OFFSET']) ? $options['OFFSET']
// : false);
//}
//if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
//if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
# Various MySQL extensions
if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
//if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
//if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
//if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
//if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
//if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
//if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
//if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
$useIndex = $this->useIndexClause( $options['USE INDEX'] );
} else {
$useIndex = '';
}
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
/**
* SELECT wrapper
*
* @param $table Mixed: Array or string, table name(s) (prefix auto-added)
* @param $vars Mixed: Array or string, field name(s) to be retrieved
* @param $conds Mixed: Array or string, condition(s) for WHERE
* @param $fname String: Calling function name (use __METHOD__) for logs/profiling
* @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
* see Database::makeSelectOptions code for list of supported stuff
* @param $join_conds Array: Associative array of table join conditions (optional)
* (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
{
if( is_array( $vars ) ) {
$vars = implode( ',', $vars );
}
if( !is_array( $options ) ) {
$options = array( $options );
}
if( is_array( $table ) ) {
if ( !empty($join_conds) || isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) )
$from = ' FROM ' . $this->tableNamesWithUseIndexOrJoin( $table, array(), $join_conds );
else
$from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
} elseif ($table!='') {
if ($table{0}==' ') {
$from = ' FROM ' . $table;
} else {
$from = ' FROM ' . $this->tableName( $table );
}
} else {
$from = '';
}
list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
if( !empty( $conds ) ) {
if ( is_array( $conds ) ) {
$conds = $this->makeList( $conds, LIST_AND );
}
$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
} else {
$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
}
if (isset($options['LIMIT']))
$sql = $this->limitResult($sql, $options['LIMIT'],
isset($options['OFFSET']) ? $options['OFFSET'] : false);
$sql = "$sql $postLimitTail";
if (isset($options['EXPLAIN'])) {
$sql = 'EXPLAIN ' . $sql;
}
return $this->query( $sql, $fname );
}
/**
* Single row SELECT wrapper
* Aborts or returns FALSE on error
*
* @param $table String: table name
* @param $vars String: the selected variables
* @param $conds Array: a condition map, terms are ANDed together.
* Items with numeric keys are taken to be literal conditions
* Takes an array of selected variables, and a condition map, which is ANDed
* e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
* NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
* $obj- >page_id is the ID of the Astronomy article
* @param $fname String: Calling functio name
* @param $options Array
* @param $join_conds Array
*
* @todo migrate documentation to phpdocumentor format
*/
function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
#$options['LIMIT'] = 1;
$res = $this->select( $table, $vars, $conds, $fname, $options );
if ( $res === false )
return false;
if ( $res->result->columnCount() == 0 ) {
$this->freeResult($res);
return false;
}
$obj = $this->fetchObject( $res );
$this->freeResult( $res );
return $obj;
}
/**
* Removes most variables from an SQL query and replaces them with X or N for numbers.
* It's only slightly flawed. Don't use for anything important.
*
* @param string $sql A SQL Query
* @static
*/
static function generalizeSQL( $sql ) {
# This does the same as the regexp below would do, but in such a way
# as to avoid crashing php on some large strings.
# $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
$sql = str_replace ( "\\\\", '', $sql);
$sql = str_replace ( "\\'", '', $sql);
$sql = str_replace ( "\\\"", '', $sql);
$sql = preg_replace ("/'.*'/s", "'X'", $sql);
$sql = preg_replace ('/".*"/s', "'X'", $sql);
# All newlines, tabs, etc replaced by single space
$sql = preg_replace ( '/\s+/', ' ', $sql);
# All numbers => N
$sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
return $sql;
}
/**
* Determines whether a field exists in a table
* Usually aborts on failure
* If errors are explicitly ignored, returns NULL on failure
*/
function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
#$table = $this->tableName( $table );
$res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '" . $table
. "' AND column_name = '" . $field . "'", $fname );
if ( !$res ) {
return NULL;
}
$found = false;
while ( $row = $this->fetchObject( $res ) ) {
if ( $row->COLUMN_NAME == $field ) {
$found = true;
break;
}
}
return $found;
}
/**
* Determines whether an index exists
* Usually aborts on failure
* If errors are explicitly ignored, returns NULL on failure
*/
function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
$info = $this->indexInfo( $table, $index, $fname );
if ( is_null( $info ) ) {
return NULL;
} else {
return $info !== false;
}
}
/**
* Get information about an index into an object
* Returns false if the index does not exist
*/
function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
# This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
# returned value except to check for the existance of indexes.
#$table = $this->tableName( $table );
$sql = "sp_helpindex '" .$table . "'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
return NULL;
}
$result = array();
while ( $row = $res->fetchObject() ) {
if ( $row->index_name == $index ) {
$row->Non_unique = !stristr($row->index_description, "unique");
$cols = explode(", ", $row->index_keys);
foreach ($cols as $col) {
$row->Column_name = trim($col);
$result[] = clone $row;
}
} else if ( $index == 'PRIMARY' && stristr($row->index_description, 'PRIMARY') ) {
$row->Non_unique = 0;
$cols = explode(", ", $row->index_keys);
foreach ($cols as $col) {
$row->Column_name = trim($col);
$result[] = clone $row;
}
}
}
#$this->freeResult($res);
return empty($result) ? false : $result;
}
/**
* Query whether a given table exists
*/
function tableExists( $table ) {
$qs = "SELECT COUNT(*) AS numtables FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '" . $table . "'";
$res = $this->mConn->query($qs);
$retVal = $res->fetchColumn();
if( $retVal ) {
#$this->freeResult( $res );
return true;
} else {
return false;
}
}
/**
* mssql_fetch_field() wrapper
* Returns false if the field doesn't exist
*
* @param $table
* @param $field
*/
function fieldInfo( $table, $field ) {
#$table = $this->tableName( $table );
$res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='". $table
. "' AND COLUMN_NAME='" . $field . "'" );
$colinfo = $res->FetchObject(true);
if (! is_object($colinfo) )
return false;
// We don't need any particular kind of object, just one with the fields the caller might expect.
$retValue = new ADOdbMetaData();
$retValue->name = $colinfo->COLUMN_NAME;
$retValue->table = $colinfo->TABLE_NAME;
$retValue->def = $colinfo->COLUMN_DEFAULT;
$retValue->max_length = $colinfo->CHARACTER_MAXIMUM_LENGTH;
$retValue->not_null = $colinfo->IS_NULLABLE == "NO";
$retValue->primary_key = 0; // temporary
$retValue->unique_key = 0; // temporarynn
$retValue->multiple_key = 0; // temporary
$retValue->blob = 0; // temporary
$retValue->type = $colinfo->DATA_TYPE;
$retValue->unsigned = 0;
$retValue->zerofill = 0;
$vres = $this->query("SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME='" . $table . "'");
if ( $vres->NumRows() > 0 )
$res = $this->query( "sp_helpindex '" . $table . "2'");
else
$res = $this->query( "sp_helpindex '" . $table . "'" );
while ( $helpInfo = $res->FetchNextObj() ) {
$keys = explode(", ", $helpInfo->index_keys);
if ( in_array($field, $keys) ) {
if ( stristr($helpInfo->index_description, "primary_key") ) {
$retValue->primary_key = 1;
}
if ( stristr($helpInfo->index_description, "unique") ) {
$retValue->unique_key = 1;
$retValue->Non_unique = 0;
} else {
$retValue->multiple_key = 1;
}
}
}
return $retValue;
}
/**
* mssql_field_type() wrapper
*/
function fieldType($res, $index ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
return $res;
} else {
$fo = $res->getColumnMeta($index);
return $fo['sqlserv:decl_type'];
}
}
/**
* INSERT wrapper, inserts an array into a table
*
* $a may be a single associative array, or an array of these with numeric keys, for
* multi-row insert.
*
* Usually aborts on failure
* If errors are explicitly ignored, returns success
*/
function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $a ) ) {
return true;
}
$table = $this->tableName( $table );
if ( !is_array( $options ) ) {
$options = array( $options );
}
if ( isset( $a[0] ) && is_array( $a[0] ) ) {
$multi = true;
$keys = array_keys( $a[0] );
$values = array_values ( $a[0] );
$record = $a[0];
} else {
$multi = false;
$keys = array_keys( $a );
$values = array_values( $a );
$record = $a;
}
// Microsoft SQL Server doesn't allow any options on an INSERT
$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES (';
$first = true;
foreach ( $keys as $row ) {
if ( $first ) {
$first = false;
$sql .= '?';
} else {
$sql .= ',?';
}
}
$sql .= ')';
$ok = false;
try {
$pdoStmt = $this->mConn->prepare( $sql );
$this->lastPDOStatement = $pdoStmt;
$ok = $pdoStmt->execute( $values );
}
catch (PDOException $ex)
{
if (! in_array('IGNORE', $options) ) {
$errorTriple = $this->mConn->errorInfo();
throw new DBQueryError( $this, $ex->errorInfo[2], $this->mConn->errorCode(), $sql, $fname );
}
}
return $ok;
}
/**
* Execute SQL
*
* @param sql SQL statement to execute, or possibly an array holding prepared statement ($sql[0] will hold sql text)
* @param [inputarr] holds the input data to bind to. Null elements will be set to null.
* @return RecordSet or false
*/
function Execute($sql,$inputarr=false)
{
if ($this->fnExecute) {
$fn = $this->fnExecute;
$ret = $fn($this,$sql,$inputarr);
if (isset($ret)) return $ret;
}
if ($inputarr) {
if (!is_array($inputarr)) $inputarr = array($inputarr);
$element0 = reset($inputarr);
# is_object check because oci8 descriptors can be passed in
$array_2d = is_array($element0);// && !is_object(reset($element0));// Removed this check 30 JUL 2008 to reduce breakage BY: djb
//remove extra memory copy of input -mikefedyk
unset($element0);
if (!is_array($sql) && !$this->_bindInputArray) {
$sqlarr = explode('?',$sql);
if (!$array_2d) $inputarr = array($inputarr);
foreach($inputarr as $arr) {
$sql = ''; $i = 0;
//Use each() instead of foreach to reduce memory usage -mikefedyk
while(list(, $v) = each($arr)) {
$sql .= $sqlarr[$i];
// from Ron Baldwin <ron.baldwin#sourceprose.com>
// Only quote string types
$typ = gettype($v);
if ($typ == 'string')
//New memory copy of input created here -mikefedyk
$sql .= $this->qstr($v);
else if ($typ == 'double')
$sql .= str_replace(',','.',$v); // locales fix so 1.1 does not get converted to 1,1
else if ($typ == 'boolean')
$sql .= $v ? $this->true : $this->false;
else if ($typ == 'object') {
if (method_exists($v, '__toString')) $sql .= $this->qstr($v->__toString());
else $sql .= $this->qstr((string) $v);
} else if ($v === null)
$sql .= 'NULL';
else
$sql .= $v;
$i += 1;
}
if (isset($sqlarr[$i])) {
$sql .= $sqlarr[$i];
if ($i+1 != sizeof($sqlarr)) $this->outp_throw( "Input Array does not match ?: ".htmlspecialchars($sql),'Execute');
} else if ($i != sizeof($sqlarr))
$this->outp_throw( "Input array does not match ?: ".htmlspecialchars($sql),'Execute');
$ret = $this->_Execute($sql);
if (!$ret) return $ret;
}
} else {
if ($array_2d) {
if (is_string($sql))
$stmt = $this->Prepare($sql);
else
$stmt = $sql;
foreach($inputarr as $arr) {
$ret = $this->_Execute($stmt,$arr);
if (!$ret) return $ret;
}
} else {
$ret = $this->_Execute($sql,$inputarr);
}
}
} else {
$ret = $this->_Execute($sql,false);
}
return $ret;
}
/**
* Change the current database
*/
function selectDB( $db ) {
$this->mDBname = $db;
$cmd = "USE " . $db;
return $this->mConn->exec( $cmd );
}
/**
* Format a table name ready for use in constructing an SQL query
*
* This does two important things: it brackets table names which as necessary,
* and it adds a table prefix if there is one.
*
* All functions of this object which require a table name call this function
* themselves. Pass the canonical name to such functions. This is only needed
* when calling query() directly.
*
* @param string $name database table name
*/
function tableName( $name ) {
global $wgSharedDB;
# Skip quoted literals
if ( $name{0} != '[' ) {
if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
$name = "{$this->mTablePrefix}$name";
}
if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
$name = "[$wgSharedDB].[$name]";
} else {
# Standard quoting
$name = "[$name]";
}
}
return $name;
}
/**
* @private
*/
function tableNamesWithUseIndexOrJoin( $tables, $use_index = array(), $join_conds = array() ) {
$ret = array();
$retJOIN = array();
$use_index_safe = is_array($use_index) ? $use_index : array();
$join_conds_safe = is_array($join_conds) ? $join_conds : array();
foreach ( $tables as $table ) {
// Is there a JOIN and INDEX clause for this table?
if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
$tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
$tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
$tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
$retJOIN[] = $tableClause;
// Is there an INDEX clause?
} else if ( isset($use_index_safe[$table]) ) {
$tableClause = $this->tableName( $table );
$tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
$ret[] = $tableClause;
// Is there a JOIN clause?
} else if ( isset($join_conds_safe[$table]) ) {
$tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
$tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
$retJOIN[] = $tableClause;
} else {
$tableClause = $this->tableName( $table );
$ret[] = $tableClause;
}
}
// We can't separate explicit JOIN clauses with ',', use ' ' for those
$straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
$otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
// Compile our final table clause
return implode(' ',array($straightJoins,$otherJoins) );
}
/**
* Wrapper for addslashes()
* @param string $s String to be slashed.
* @return string slashed string.
*/
function strencode( $s ) {
$s = $this->qstr($s, get_magic_quotes_gpc());
#$s = substr($s, 1, -1); // qstr adds quotes, which the caller is presumably going to do again.
return $s;
}
/**
* Correctly quotes a string so that all strings are escaped. We prefix and append
* to the string single-quotes.
* An example is $db->qstr("Don't bother",magic_quotes_runtime());
*
* @param s the string to quote
* @param [magic_quotes] if $s is GET/POST var, set to get_magic_quotes_gpc().
* This undoes the stupidity of magic quotes for GPC.
*
* @return quoted string to be sent back to database
*/
function qstr($s,$magic_quotes=false)
{
return $this->mConn->quote($s);
// In case the above doesn't work!
if (!$magic_quotes) {
if ($this->replaceQuote[0] == '\\'){
// only since php 4.0.5
$s = str_replace(array('\\',"\0"),array('\\\\',"\\\0"),$s);
//$s = str_replace("\0","\\\0", str_replace('\\','\\\\',$s));
}
return "'".str_replace("'",$this->replaceQuote,$s)."'";
}
// undo magic quotes for "
$s = str_replace('\\"','"',$s);
if ($this->replaceQuote == "\\'") // ' already quoted, no need to change anything
return "'$s'";
else {// change \' to '' for sybase/mssql
$s = str_replace('\\\\','\\',$s);
return "'".str_replace("\\'",$this->replaceQuote,$s)."'";
}
}
/**
* If it's a string, adds quotes and backslashes
* Otherwise returns as-is
*/
function addQuotes( $s ) {
if ( is_null( $s ) ) {
return 'NULL';
} else if ( is_numeric( $s ) ) {
return "'" . $s . "'";
} else {
# This will also quote numeric values. This should be harmless,
# and protects against weird problems that occur when they really
# _are_ strings such as article titles and string->number->string
# conversion is not 1:1.
return $this->strencode( $s );
}
}
/**
* Escape string for safe LIKE usage
*/
function escapeLike( $s ) {
//$s=$this->strencode( $s );
$s=str_replace(array('%','_'),array('\%','\_'),$s);
return $s;
}
/**
* USE INDEX clause
* SQL Server doesn't have them and returns ""
*/
function useIndexClause( $index ) {
return "";
}
/**
* REPLACE query wrapper
* PostgreSQL simulates this with a DELETE followed by INSERT
* $row is the row to insert, an associative array
* $uniqueIndexes is an array of indexes. Each element may be either a
* field name or an array of field names
*
* It may be more efficient to leave off unique indexes which are unlikely to collide.
* However if you do this, you run the risk of encountering errors which wouldn't have
* occurred in MySQL
*
* @todo migrate comment to phodocumentor format
*/
function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
$table = $this->tableName( $table );
# Single row case
if ( !is_array( reset( $rows ) ) ) {
$rows = array( $rows );
}
foreach( $rows as $row ) {
# Delete rows which collide
if ( $uniqueIndexes ) {
$sql = "DELETE FROM $table WHERE ";
$first = true;
foreach ( $uniqueIndexes as $index ) {
if ( $first ) {
$first = false;
$sql .= "(";
} else {
$sql .= ') OR (';
}
if ( is_array( $index ) ) {
$first2 = true;
foreach ( $index as $col ) {
if ( $first2 ) {
$first2 = false;
} else {
$sql .= ' AND ';
}
$sql .= $col.'=' . $this->addQuotes( $row[$col] );
}
} else {
$sql .= $index.'=' . $this->addQuotes( $row[$index] );
}
}
$sql .= ')';
try {
$this->query( $sql, $fname );
} catch (ADODB_Exception $exc) {
}
}
# Now insert the row
$sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
$this->makeList( $row, LIST_COMMA ) . ')';
$this->query( $sql, $fname );
}
}
/**
* DELETE where the condition is a join
* MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
* Still need to check what SQL Server should do. For now, this is the same as the inherited implementation.
* For safety, an empty $conds will not delete everything. If you want to delete all rows where the
* join condition matches, set $conds='*'
*
* DO NOT put the join condition in $conds
*
* @param $delTable String: The table to delete from.
* @param $joinTable String: The other table.
* @param $delVar String: The variable to join on, in the first table.
* @param $joinVar String: The variable to join on, in the second table.
* @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
* @param $fname String: Calling function name (use __METHOD__) for logs/profiling
*/
function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
if ( !$conds ) {
throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
}
$delTable = $this->tableName( $delTable );
$joinTable = $this->tableName( $joinTable );
$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
if ( $conds != '*' ) {
$sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
}
return $this->query( $sql, $fname );
}
/**
* @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
*/
function lowPriorityOption() {
return '';
}
/**
* Construct a LIMIT query with optional offset
* This is used for query pages
* $sql string SQL query we will append the limit too
* $limit integer the SQL limit
* $offset integer the SQL offset (default false)
*/
function limitResult($sql, $limit, $offset=false) {
if( !is_numeric($limit) ) {
throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
if ( is_numeric($offset) && $offset != 0) {
$this->limitOffset = $offset;
$limit += $offset;
} else {
$this->limitOffset = 0;
}
if ( strpos($sql, "SELECT") === false ) {
return "TOP {$limit} " . $sql;
} else {
return preg_replace("/SELECT/", "SELECT TOP {$limit}", $sql);
}
}
function limitResultForUpdate($sql, $num) {
# Can't really do this on Microsoft SQL?
return $sql;
#return $this->limitResult($sql, $num, 0);
}
/**
* Returns an SQL expression for a simple conditional.
* Uses CASE on SQL Server
*
* @param string $cond SQL expression which will result in a boolean value
* @param string $trueVal SQL expression to return if true
* @param string $falseVal SQL expression to return if false
* @return string SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
return " CASE WHEN $cond THEN $trueVal ELSE $falseVal END ";
}
/**
* Determines if the last failure was due to a deadlock
*/
function wasDeadlock() {
return $this->lastErrno() == 1213;
}
/**
* Perform a deadlock-prone transaction.
*
* This function invokes a callback function to perform a set of write
* queries. If a deadlock occurs during the processing, the transaction
* will be rolled back and the callback function will be called again.
*
* Usage:
* $dbw->deadlockLoop( callback, ... );
*
* Extra arguments are passed through to the specified callback function.
*
* Returns whatever the callback function returned on its successful,
* iteration, or false on error, for example if the retry limit was
* reached.
*/
function deadlockLoop() {
$myFname = 'Database::deadlockLoop';
$this->begin();
$args = func_get_args();
$function = array_shift( $args );
$oldIgnore = $this->ignoreErrors( true );
$tries = DEADLOCK_TRIES;
if ( is_array( $function ) ) {
$fname = $function[0];
} else {
$fname = $function;
}
do {
$retVal = call_user_func_array( $function, $args );
$error = $this->lastError();
$errno = $this->lastErrno();
$sql = $this->lastQuery();
if ( $errno ) {
if ( $this->wasDeadlock() ) {
# Retry
usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
} else {
$this->reportQueryError( $error, $errno, $sql, $fname );
}
}
} while( $this->wasDeadlock() && --$tries > 0 );
$this->ignoreErrors( $oldIgnore );
if ( $tries <= 0 ) {
$this->rollback();
$this->reportQueryError( $error, $errno, $sql, $fname );
return false;
} else {
$this->commit();
return $retVal;
}
}
/**
* Get the position of the master from SHOW SLAVE STATUS
*/
function getSlavePos() {
$res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
$row = $this->fetchObj( $res );
if ( $row ) {
return array( $row->Master_Log_File, $row->Read_Master_Log_Pos );
} else {
return array( false, false );
}
}
/**
* Get the position of the master from SHOW MASTER STATUS
*/
function getMasterPos() {
$res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
$row = $this->fetchObj( $res );
if ( $row ) {
return array( $row->File, $row->Position );
} else {
return array( false, false );
}
}
/**
* Begin a transaction, committing any previously open transaction
*/
function begin( $fname = 'Database::begin' ) {
try {
$this->mConn->commit();
} catch (PDOException $e)
{
}
try {
if ( $this->mTrxLevel != 1 )
$this->mConn->beginTransaction();
$this->mTrxLevel = 1;
} catch (PDOException $e) {
}
}
/**
* End a transaction
*/
function commit( $fname = 'Database::commit' ) {
try {
if ($this->mTrxLevel == 1 )
$this->mConn->commit();
$this->mTrxLevel = 0;
} catch (PDOException $e) {
}
}
/**
* Rollback a transaction
*/
function rollback( $fname = 'Database::rollback' ) {
try {
if ( $this->mTrxLevel == 1 )
$this->mConn->rollback();
$this->mTrxLevel = 0;
} catch (PDOException $e) {
}
}
/**
* @return string wikitext of a link to the server software's web site
*/
function getSoftwareLink() {
return "[http://www.microsoft.com/download/en/details.aspx?id=2098 Microsoft Drivers for PHP for SQL Server]";
}
/**
* @return string Version information from the database
*/
function getServerVersion() {
$result = $this->mConn->query("SELECT @@VERSION AS version");
return $result->fetchColumn();
}
/**
* Ping the server and try to reconnect if it there is no connection
*/
function ping() {
wfDebug( "Tried to call mssql_ping but this is the SQL Server version. Faking it!\n" );
return true;
}
/**
* Get slave lag.
* At the moment, this will only work if the DB user has the PROCESS privilege
*/
function getLag() {
return false; // Still need to figure out what to do on SQL Server
$res = $this->query( 'SHOW PROCESSLIST' );
# Find slave SQL thread. Assumed to be the second one running, which is a bit
# dubious, but unfortunately there's no easy rigorous way
while ( $row = $this->FetchNextObj( $res ) ) {
/* This should work for most situations - when default db
* for thread is not specified, it had no events executed,
* and therefore it doesn't know yet how lagged it is.
*
* Relay log I/O thread does not select databases.
*/
if ( $row->User == 'system user' &&
$row->State != 'Waiting for master to send event' &&
$row->State != 'Connecting to master' &&
$row->State != 'Queueing master event to the relay log' &&
$row->State != 'Waiting for master update' &&
$row->State != 'Requesting binlog dump'
) {
# This is it, return the time (except -ve)
if ( $row->Time > 0x7fffffff ) {
return false;
} else {
return $row->Time;
}
}
}
return false;
}
/**
* Get status information from SHOW STATUS in an associative array
*/
function getStatus($which="%") {
return false; // Still need to figure this out for SQL Server
$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
$status = array();
while ( $row = $this->FetchNextObj( $res ) ) {
$status[$row->Variable_name] = $row->Value;
}
return $status;
}
function encodeBlob($b) {
return utf8_encode( $b );
//return $this->mConn->BlobEncode($b);
}
function decodeBlob($b) {
return utf8_decode( $b );
//return $this->mConn->BlobDecode($b);
}
/**
* Override database's default connection timeout.
* May be useful for very long batch queries such as
* full-wiki dumps, where a single query reads out
* over hours or days.
* @param int $timeout in seconds
*/
public function setTimeout( $timeout ) {
//$this->query( "SET net_read_timeout=$timeout" );
//$this->query( "SET net_write_timeout=$timeout" );
}
function setup_database() {
global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport;
dbsource( "../maintenance/sqlserver/tables.sql", $this);
## Update version information
$mwv = $this->addQuotes($wgVersion);
$pgv = $this->addQuotes($this->getServerVersion());
$pgu = $this->addQuotes($this->mUser);
$mws = $this->addQuotes($wgDBmwschema);
$tss = $this->addQuotes($wgDBts2schema);
$pgp = $this->addQuotes($wgDBport);
$dbn = $this->addQuotes($this->mDBname);
$ctype = "UNKNOWN";
//$ctype = odbc_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
$SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
"mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
"ctype = '$ctype' ".
"WHERE type = 'Creation'";
$this->query($SQL);
## Avoid the non-standard "REPLACE INTO" syntax
$f = fopen( "../maintenance/interwiki.sql", 'r' );
if ($f == false ) {
dieout( "<li>Could not find the interwiki.sql file");
}
## We simply assume it is already empty as we have just created it
$SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
while ( ! feof( $f ) ) {
$line = fgets($f,1024);
$matches = array();
if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
continue;
}
$this->query("$SQL $matches[1],$matches[2])");
}
print " (table interwiki successfully populated)...\n";
}
/**
* Replace variables in sourced SQL
*/
protected function replaceVars( $ins ) {
$varnames = array(
'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
'wgDBadminuser', 'wgDBadminpassword',
);
// Ordinary variables
foreach ( $varnames as $var ) {
if( isset( $GLOBALS[$var] ) ) {
$val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
$ins = str_replace( '{$' . $var . '}', $val, $ins );
$ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
$ins = str_replace( '/*$' . $var . '*/', $val, $ins );
}
}
// Table prefixes
$ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z0-9_]*)/',
array( &$this, 'tableNameCallback' ), $ins );
return $ins;
}
function getDBname() {
return $this->mDBname;
}
function getServer() {
return $this->mServer;
}
/*
* Build a concatenation list to feed into a SQL query
*/
function buildConcat( $stringList ) {
//return join($stringList, ' + ');
return implode(' + ', $stringList);
}
function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabaseMSPDO::estimateRowCount', $options = array() ) {
//$options['EXPLAIN']=true;
$res = $this->select ($table, $vars, $conds, $fname, $options );
if ( $res === false )
return false;
$rows = $this->numRows($res);
$this->freeResult($res);
if (!$rows) {
return 0;
}
return $rows;
}
/**
* Returns true if this database does an implicit sort when doing GROUP BY
*/
function implicitGroupby() {
return false;
}
public function setBigSelects( $value = true ) {
// no-op
}
}

File Metadata

Mime Type
text/x-php
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3356
Default Alt Text
DatabaseMSPDO.php (46 KB)

Event Timeline