Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4074236
Database.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
108 KB
Referenced Files
None
Subscribers
None
Database.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
Wikimedia\Rdbms
;
use
InvalidArgumentException
;
use
LogicException
;
use
Psr\Log\LoggerAwareInterface
;
use
Psr\Log\LoggerInterface
;
use
Psr\Log\NullLogger
;
use
RuntimeException
;
use
Stringable
;
use
Throwable
;
use
Wikimedia\AtEase\AtEase
;
use
Wikimedia\Rdbms\Database\DatabaseFlags
;
use
Wikimedia\Rdbms\Platform\SQLPlatform
;
use
Wikimedia\Rdbms\Replication\ReplicationReporter
;
use
Wikimedia\RequestTimeout\CriticalSectionProvider
;
use
Wikimedia\RequestTimeout\CriticalSectionScope
;
use
Wikimedia\ScopedCallback
;
/**
* A single concrete connection to a relational database.
*
* This is the base class for all connection-specific relational database handles.
* No two instances of this class should share the same underlying network connection.
*
* @see IDatabase
* @ingroup Database
* @since 1.28
*/
abstract
class
Database
implements
Stringable
,
IDatabaseForOwner
,
IMaintainableDatabase
,
LoggerAwareInterface
{
/** @var CriticalSectionProvider|null */
protected
$csProvider
;
/** @var LoggerInterface */
protected
$logger
;
/** @var callable Error logging callback */
protected
$errorLogger
;
/** @var callable Deprecation logging callback */
protected
$deprecationLogger
;
/** @var callable|null */
protected
$profiler
;
/** @var TransactionManager */
private
$transactionManager
;
/** @var DatabaseDomain */
protected
$currentDomain
;
/** @var DatabaseFlags */
protected
$flagsHolder
;
// phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.ObjectTypeHintVar
/** @var object|resource|null Database connection */
protected
$conn
;
/** @var string|null Readable name or host/IP of the database server */
protected
$serverName
;
/** @var bool Whether this PHP instance is for a CLI script */
protected
$cliMode
;
/** @var int|null Maximum seconds to wait on connection attempts */
protected
$connectTimeout
;
/** @var int|null Maximum seconds to wait on receiving query results */
protected
$receiveTimeout
;
/** @var string Agent name for query profiling */
protected
$agent
;
/** @var array<string,mixed> Connection parameters used by initConnection() and open() */
protected
$connectionParams
;
/** @var string[]|int[]|float[] SQL variables values to use for all new connections */
protected
$connectionVariables
;
/** @var int Row batch size to use for emulated INSERT SELECT queries */
protected
$nonNativeInsertSelectBatchSize
;
/** @var bool Whether to use SSL connections */
protected
$ssl
;
/** @var bool Whether to check for warnings */
protected
$strictWarnings
;
/** @var array Current LoadBalancer tracking information */
protected
$lbInfo
=
[];
/** @var string|false Current SQL query delimiter */
protected
$delimiter
=
';'
;
/** @var string|bool|null Stashed value of html_errors INI setting */
private
$htmlErrors
;
/** @var array<string,array> Map of (lock name => (UNIX time,trx ID)) */
protected
$sessionNamedLocks
=
[];
/** @var array<string,array<string, TempTableInfo>> Map of (DB name => table name => info) */
protected
$sessionTempTables
=
[];
/** @var int Affected row count for the last statement to query() */
protected
$lastQueryAffectedRows
=
0
;
/** @var int|null Insert (row) ID for the last statement to query() (null if not supported) */
protected
$lastQueryInsertId
;
/** @var int|null Affected row count for the last query method call; null if unspecified */
protected
$lastEmulatedAffectedRows
;
/** @var int|null Insert (row) ID for the last query method call; null if unspecified */
protected
$lastEmulatedInsertId
;
/** @var string Last error during connection; empty string if none */
protected
$lastConnectError
=
''
;
/** @var float UNIX timestamp of the last server response */
private
$lastPing
=
0.0
;
/** @var float|false UNIX timestamp of last write query */
private
$lastWriteTime
=
false
;
/** @var string|false The last PHP error from a query or connection attempt */
private
$lastPhpError
=
false
;
/** @var int|null Current critical section numeric ID */
private
$csmId
;
/** @var string|null Last critical section caller name */
private
$csmFname
;
/** @var DBUnexpectedError|null Last unresolved critical section error */
private
$csmError
;
/** Whether the database is a file on disk */
public
const
ATTR_DB_IS_FILE
=
'db-is-file'
;
/** Lock granularity is on the level of the entire database */
public
const
ATTR_DB_LEVEL_LOCKING
=
'db-level-locking'
;
/** The SCHEMA keyword refers to a grouping of tables in a database */
public
const
ATTR_SCHEMAS_AS_TABLE_GROUPS
=
'supports-schemas'
;
/** New Database instance will not be connected yet when returned */
public
const
NEW_UNCONNECTED
=
0
;
/** New Database instance will already be connected when returned */
public
const
NEW_CONNECTED
=
1
;
/** No errors occurred during the query */
protected
const
ERR_NONE
=
0
;
/** Retry query due to a connection loss detected while sending the query (session intact) */
protected
const
ERR_RETRY_QUERY
=
1
;
/** Abort query (no retries) due to a statement rollback (session/transaction intact) */
protected
const
ERR_ABORT_QUERY
=
2
;
/** Abort any current transaction, by rolling it back, due to an error during the query */
protected
const
ERR_ABORT_TRX
=
4
;
/** Abort and reset session due to server-side session-level state loss (locks, temp tables) */
protected
const
ERR_ABORT_SESSION
=
8
;
/** Assume that queries taking this long to yield connection loss errors are at fault */
protected
const
DROPPED_CONN_BLAME_THRESHOLD_SEC
=
3.0
;
/** @var string Idiom used when a cancelable atomic section started the transaction */
private
const
NOT_APPLICABLE
=
'n/a'
;
/** How long before it is worth doing a dummy query to test the connection */
private
const
PING_TTL
=
1.0
;
/** Dummy SQL query */
private
const
PING_QUERY
=
'SELECT 1 AS ping'
;
/** Hostname or IP address to use on all connections */
protected
const
CONN_HOST
=
'host'
;
/** Database server username to use on all connections */
protected
const
CONN_USER
=
'user'
;
/** Database server password to use on all connections */
protected
const
CONN_PASSWORD
=
'password'
;
/** Database name to use on initial connection */
protected
const
CONN_INITIAL_DB
=
'dbname'
;
/** Schema name to use on initial connection */
protected
const
CONN_INITIAL_SCHEMA
=
'schema'
;
/** Table prefix to use on initial connection */
protected
const
CONN_INITIAL_TABLE_PREFIX
=
'tablePrefix'
;
/** @var SQLPlatform */
protected
$platform
;
/** @var ReplicationReporter */
protected
$replicationReporter
;
/**
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
*/
public
function
__construct
(
array
$params
)
{
$this
->
logger
=
$params
[
'logger'
]
??
new
NullLogger
();
$this
->
transactionManager
=
new
TransactionManager
(
$this
->
logger
,
$params
[
'trxProfiler'
]
);
$this
->
connectionParams
=
[
self
::
CONN_HOST
=>
(
isset
(
$params
[
'host'
]
)
&&
$params
[
'host'
]
!==
''
)
?
$params
[
'host'
]
:
null
,
self
::
CONN_USER
=>
(
isset
(
$params
[
'user'
]
)
&&
$params
[
'user'
]
!==
''
)
?
$params
[
'user'
]
:
null
,
self
::
CONN_INITIAL_DB
=>
(
isset
(
$params
[
'dbname'
]
)
&&
$params
[
'dbname'
]
!==
''
)
?
$params
[
'dbname'
]
:
null
,
self
::
CONN_INITIAL_SCHEMA
=>
(
isset
(
$params
[
'schema'
]
)
&&
$params
[
'schema'
]
!==
''
)
?
$params
[
'schema'
]
:
null
,
self
::
CONN_PASSWORD
=>
is_string
(
$params
[
'password'
]
)
?
$params
[
'password'
]
:
null
,
self
::
CONN_INITIAL_TABLE_PREFIX
=>
(
string
)
$params
[
'tablePrefix'
]
];
$this
->
lbInfo
=
$params
[
'lbInfo'
]
??
[];
$this
->
connectionVariables
=
$params
[
'variables'
]
??
[];
// Set SQL mode, default is turning them all off, can be overridden or skipped with null
if
(
is_string
(
$params
[
'sqlMode'
]
??
null
)
)
{
$this
->
connectionVariables
[
'sql_mode'
]
=
$params
[
'sqlMode'
];
}
$flags
=
(
int
)
$params
[
'flags'
];
$this
->
flagsHolder
=
new
DatabaseFlags
(
$flags
);
$this
->
ssl
=
$params
[
'ssl'
]
??
(
bool
)(
$flags
&
self
::
DBO_SSL
);
$this
->
connectTimeout
=
$params
[
'connectTimeout'
]
??
null
;
$this
->
receiveTimeout
=
$params
[
'receiveTimeout'
]
??
null
;
$this
->
cliMode
=
(
bool
)
$params
[
'cliMode'
];
$this
->
agent
=
(
string
)
$params
[
'agent'
];
$this
->
serverName
=
$params
[
'serverName'
];
$this
->
nonNativeInsertSelectBatchSize
=
$params
[
'nonNativeInsertSelectBatchSize'
]
??
10000
;
$this
->
strictWarnings
=
!
empty
(
$params
[
'strictWarnings'
]
);
$this
->
profiler
=
is_callable
(
$params
[
'profiler'
]
)
?
$params
[
'profiler'
]
:
null
;
$this
->
errorLogger
=
$params
[
'errorLogger'
];
$this
->
deprecationLogger
=
$params
[
'deprecationLogger'
];
$this
->
csProvider
=
$params
[
'criticalSectionProvider'
]
??
null
;
// Set initial dummy domain until open() sets the final DB/prefix
$this
->
currentDomain
=
new
DatabaseDomain
(
$params
[
'dbname'
]
!=
''
?
$params
[
'dbname'
]
:
null
,
$params
[
'schema'
]
!=
''
?
$params
[
'schema'
]
:
null
,
$params
[
'tablePrefix'
]
);
$this
->
platform
=
new
SQLPlatform
(
$this
,
$this
->
logger
,
$this
->
currentDomain
,
$this
->
errorLogger
);
// Children classes must set $this->replicationReporter.
}
/**
* Initialize the connection to the database over the wire (or to local files)
*
* @throws LogicException
* @throws InvalidArgumentException
* @throws DBConnectionError
* @since 1.31
*/
final
public
function
initConnection
()
{
if
(
$this
->
isOpen
()
)
{
throw
new
LogicException
(
__METHOD__
.
': already connected'
);
}
// Establish the connection
$this
->
open
(
$this
->
connectionParams
[
self
::
CONN_HOST
],
$this
->
connectionParams
[
self
::
CONN_USER
],
$this
->
connectionParams
[
self
::
CONN_PASSWORD
],
$this
->
connectionParams
[
self
::
CONN_INITIAL_DB
],
$this
->
connectionParams
[
self
::
CONN_INITIAL_SCHEMA
],
$this
->
connectionParams
[
self
::
CONN_INITIAL_TABLE_PREFIX
]
);
$this
->
lastPing
=
microtime
(
true
);
}
/**
* Open a new connection to the database (closing any existing one)
*
* @param string|null $server Server host/address and optional port {@see connectionParams}
* @param string|null $user User name {@see connectionParams}
* @param string|null $password User password {@see connectionParams}
* @param string|null $db Database name
* @param string|null $schema Database schema name
* @param string $tablePrefix
* @throws DBConnectionError
*/
abstract
protected
function
open
(
$server
,
$user
,
$password
,
$db
,
$schema
,
$tablePrefix
);
/**
* @return array Map of (Database::ATTR_* constant => value)
* @since 1.31
*/
public
static
function
getAttributes
()
{
return
[];
}
/**
* Set the PSR-3 logger interface to use.
*
* @param LoggerInterface $logger
*/
public
function
setLogger
(
LoggerInterface
$logger
)
{
$this
->
logger
=
$logger
;
}
public
function
getServerInfo
()
{
return
$this
->
getServerVersion
();
}
public
function
tablePrefix
(
$prefix
=
null
)
{
$old
=
$this
->
currentDomain
->
getTablePrefix
();
if
(
$prefix
!==
null
)
{
$this
->
currentDomain
=
new
DatabaseDomain
(
$this
->
currentDomain
->
getDatabase
(),
$this
->
currentDomain
->
getSchema
(),
$prefix
);
$this
->
platform
->
setCurrentDomain
(
$this
->
currentDomain
);
}
return
$old
;
}
public
function
dbSchema
(
$schema
=
null
)
{
$old
=
$this
->
currentDomain
->
getSchema
();
if
(
$schema
!==
null
)
{
if
(
$schema
!==
''
&&
$this
->
getDBname
()
===
null
)
{
throw
new
DBUnexpectedError
(
$this
,
"Cannot set schema to '$schema'; no database set"
);
}
$this
->
currentDomain
=
new
DatabaseDomain
(
$this
->
currentDomain
->
getDatabase
(),
// DatabaseDomain uses null for unspecified schemas
(
$schema
!==
''
)
?
$schema
:
null
,
$this
->
currentDomain
->
getTablePrefix
()
);
$this
->
platform
->
setCurrentDomain
(
$this
->
currentDomain
);
}
return
(
string
)
$old
;
}
public
function
getLBInfo
(
$name
=
null
)
{
if
(
$name
===
null
)
{
return
$this
->
lbInfo
;
}
if
(
array_key_exists
(
$name
,
$this
->
lbInfo
)
)
{
return
$this
->
lbInfo
[
$name
];
}
return
null
;
}
public
function
setLBInfo
(
$nameOrArray
,
$value
=
null
)
{
if
(
is_array
(
$nameOrArray
)
)
{
$this
->
lbInfo
=
$nameOrArray
;
}
elseif
(
is_string
(
$nameOrArray
)
)
{
if
(
$value
!==
null
)
{
$this
->
lbInfo
[
$nameOrArray
]
=
$value
;
}
else
{
unset
(
$this
->
lbInfo
[
$nameOrArray
]
);
}
}
else
{
throw
new
InvalidArgumentException
(
"Got non-string key"
);
}
}
public
function
lastDoneWrites
()
{
return
$this
->
lastWriteTime
?:
false
;
}
/**
* @return bool
* @since 1.39
* @internal For use by Database/LoadBalancer only
*/
public
function
sessionLocksPending
()
{
return
(
bool
)
$this
->
sessionNamedLocks
;
}
/**
* @return string|null ID of the active explicit transaction round being participating in
*/
final
protected
function
getTransactionRoundId
()
{
if
(
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
)
{
// LoadBalancer transaction round participation is enabled for this DB handle;
// get the ID of the active explicit transaction round (if any)
$id
=
$this
->
getLBInfo
(
self
::
LB_TRX_ROUND_ID
);
return
is_string
(
$id
)
?
$id
:
null
;
}
return
null
;
}
public
function
isOpen
()
{
return
(
bool
)
$this
->
conn
;
}
public
function
getDomainID
()
{
return
$this
->
currentDomain
->
getId
();
}
/**
* Wrapper for addslashes()
*
* @param string $s String to be slashed.
* @return string Slashed string.
*/
abstract
public
function
strencode
(
$s
);
/**
* Set a custom error handler for logging errors during database connection
*/
protected
function
installErrorHandler
()
{
$this
->
lastPhpError
=
false
;
$this
->
htmlErrors
=
ini_set
(
'html_errors'
,
'0'
);
set_error_handler
(
[
$this
,
'connectionErrorLogger'
]
);
}
/**
* Restore the previous error handler and return the last PHP error for this DB
*
* @return string|false
*/
protected
function
restoreErrorHandler
()
{
restore_error_handler
();
if
(
$this
->
htmlErrors
!==
false
)
{
ini_set
(
'html_errors'
,
$this
->
htmlErrors
);
}
return
$this
->
getLastPHPError
();
}
/**
* @return string|false Last PHP error for this DB (typically connection errors)
*/
protected
function
getLastPHPError
()
{
if
(
$this
->
lastPhpError
)
{
$error
=
preg_replace
(
'!
\[
<a.*</a>
\]
!'
,
''
,
$this
->
lastPhpError
);
$error
=
preg_replace
(
'!^.*?:
\s
?(.*)$!'
,
'$1'
,
$error
);
return
$error
;
}
return
false
;
}
/**
* Error handler for logging errors during database connection
*
* @internal This method should not be used outside of Database classes
*
* @param int|string $errno
* @param string $errstr
*/
public
function
connectionErrorLogger
(
$errno
,
$errstr
)
{
$this
->
lastPhpError
=
$errstr
;
}
/**
* Create a log context to pass to PSR-3 logger functions.
*
* @param array $extras Additional data to add to context
* @return array
*/
protected
function
getLogContext
(
array
$extras
=
[]
)
{
return
array_merge
(
[
'db_server'
=>
$this
->
getServerName
(),
'db_name'
=>
$this
->
getDBname
(),
'db_user'
=>
$this
->
connectionParams
[
self
::
CONN_USER
]
??
null
,
],
$extras
);
}
final
public
function
close
(
$fname
=
__METHOD__
)
{
$error
=
null
;
// error to throw after disconnecting
$wasOpen
=
(
bool
)
$this
->
conn
;
// This should mostly do nothing if the connection is already closed
if
(
$this
->
conn
)
{
// Roll back any dangling transaction first
if
(
$this
->
trxLevel
()
)
{
$error
=
$this
->
transactionManager
->
trxCheckBeforeClose
(
$this
,
$fname
);
// Rollback the changes and run any callbacks as needed
$this
->
rollback
(
__METHOD__
,
self
::
FLUSHING_INTERNAL
);
$this
->
runTransactionPostRollbackCallbacks
();
}
// Close the actual connection in the binding handle
$closed
=
$this
->
closeConnection
();
}
else
{
$closed
=
true
;
// already closed; nothing to do
}
$this
->
conn
=
null
;
// Log any unexpected errors after having disconnected
if
(
$error
!==
null
)
{
// T217819, T231443: this is probably just LoadBalancer trying to recover from
// errors and shutdown. Log any problems and move on since the request has to
// end one way or another. Throwing errors is not very useful at some point.
$this
->
logger
->
error
(
$error
,
[
'db_log_category'
=>
'query'
]
);
}
// Note that various subclasses call close() at the start of open(), which itself is
// called by replaceLostConnection(). In that case, just because onTransactionResolution()
// callbacks are pending does not mean that an exception should be thrown. Rather, they
// will be executed after the reconnection step.
if
(
$wasOpen
)
{
// Double check that no callbacks are dangling
$fnames
=
$this
->
pendingWriteAndCallbackCallers
();
if
(
$fnames
)
{
throw
new
RuntimeException
(
"Transaction callbacks are still pending: "
.
implode
(
', '
,
$fnames
)
);
}
}
return
$closed
;
}
/**
* Make sure there is an open connection handle (alive or not)
*
* This guards against fatal errors to the binding handle not being defined in cases
* where open() was never called or close() was already called.
*
* @throws DBUnexpectedError
*/
final
protected
function
assertHasConnectionHandle
()
{
if
(
!
$this
->
isOpen
()
)
{
throw
new
DBUnexpectedError
(
$this
,
"DB connection was already closed"
);
}
}
/**
* Closes underlying database connection
* @return bool Whether connection was closed successfully
* @since 1.20
*/
abstract
protected
function
closeConnection
();
/**
* Run a query and return a QueryStatus instance with the query result information
*
* This is meant to handle the basic command of actually sending a query to the
* server via the driver. No implicit transaction, reconnection, nor retry logic
* should happen here. The higher level query() method is designed to handle those
* sorts of concerns. This method should not trigger such higher level methods.
*
* The lastError() and lastErrno() methods should meaningfully reflect what error,
* if any, occurred during the last call to this method. Methods like executeQuery(),
* query(), select(), insert(), update(), delete(), and upsert() implement their calls
* to doQuery() such that an immediately subsequent call to lastError()/lastErrno()
* meaningfully reflects any error that occurred during that public query method call.
*
* For SELECT queries, the result field contains either:
* - a) A driver-specific IResultWrapper describing the query results
* - b) False, on any query failure
*
* For non-SELECT queries, the result field contains either:
* - a) A driver-specific IResultWrapper, only on success
* - b) True, only on success (e.g. no meaningful result other than "OK")
* - c) False, on any query failure
*
* @param string $sql Single-statement SQL query
* @return QueryStatus
* @since 1.39
*/
abstract
protected
function
doSingleStatementQuery
(
string
$sql
):
QueryStatus
;
/**
* Determine whether a write query affects a permanent table.
* This includes pseudo-permanent tables.
*
* @param Query $query
* @return bool
*/
private
function
hasPermanentTable
(
Query
$query
)
{
if
(
$query
->
getVerb
()
===
'CREATE TEMPORARY'
)
{
// Temporary table creation is allowed
return
false
;
}
$table
=
$query
->
getWriteTable
();
if
(
$table
===
null
)
{
// Parse error? Assume permanent.
return
true
;
}
[
$db
,
$pt
]
=
$this
->
platform
->
getDatabaseAndTableIdentifier
(
$table
);
$tempInfo
=
$this
->
sessionTempTables
[
$db
][
$pt
]
??
null
;
return
!
$tempInfo
||
$tempInfo
->
pseudoPermanent
;
}
/**
* Register creation and dropping of temporary tables
*
* @param Query $query
*/
protected
function
registerTempTables
(
Query
$query
)
{
$table
=
$query
->
getWriteTable
();
if
(
$table
===
null
)
{
return
;
}
switch
(
$query
->
getVerb
()
)
{
case
'CREATE TEMPORARY'
:
[
$db
,
$pt
]
=
$this
->
platform
->
getDatabaseAndTableIdentifier
(
$table
);
$this
->
sessionTempTables
[
$db
][
$pt
]
=
new
TempTableInfo
(
$this
->
transactionManager
->
getTrxId
(),
(
bool
)(
$query
->
getFlags
()
&
self
::
QUERY_PSEUDO_PERMANENT
)
);
break
;
case
'DROP'
:
[
$db
,
$pt
]
=
$this
->
platform
->
getDatabaseAndTableIdentifier
(
$table
);
unset
(
$this
->
sessionTempTables
[
$db
][
$pt
]
);
}
}
public
function
query
(
$sql
,
$fname
=
__METHOD__
,
$flags
=
0
)
{
if
(
!(
$sql
instanceof
Query
)
)
{
$flags
=
(
int
)
$flags
;
// b/c; this field used to be a bool
$sql
=
QueryBuilderFromRawSql
::
buildQuery
(
$sql
,
$flags
,
$this
->
currentDomain
->
getTablePrefix
()
);
}
else
{
$flags
=
$sql
->
getFlags
();
}
// Make sure that this caller is allowed to issue this query statement
$this
->
assertQueryIsCurrentlyAllowed
(
$sql
->
getVerb
(),
$fname
);
// Send the query to the server and fetch any corresponding errors
$status
=
$this
->
executeQuery
(
$sql
,
$fname
,
$flags
);
if
(
$status
->
res
===
false
)
{
// An error occurred; log, and, if needed, report an exception.
// Errors that corrupt the transaction/session state cannot be silenced.
$ignore
=
(
$this
->
flagsHolder
::
contains
(
$flags
,
self
::
QUERY_SILENCE_ERRORS
)
&&
!
$this
->
flagsHolder
::
contains
(
$status
->
flags
,
self
::
ERR_ABORT_SESSION
)
&&
!
$this
->
flagsHolder
::
contains
(
$status
->
flags
,
self
::
ERR_ABORT_TRX
)
);
$this
->
reportQueryError
(
$status
->
message
,
$status
->
code
,
$sql
->
getSQL
(),
$fname
,
$ignore
);
}
return
$status
->
res
;
}
/**
* Execute a query without enforcing public (non-Database) caller restrictions.
*
* Retry it if there is a recoverable connection loss (e.g. no important state lost).
*
* This does not precheck for transaction/session state errors or critical section errors.
*
* @see Database::query()
*
* @param Query $sql SQL statement
* @param string $fname Name of the calling function
* @param int $flags Bit field of ISQLPlatform::QUERY_* constants
* @return QueryStatus
* @throws DBUnexpectedError
* @since 1.34
*/
final
protected
function
executeQuery
(
$sql
,
$fname
,
$flags
)
{
$this
->
assertHasConnectionHandle
();
$isPermWrite
=
false
;
$isWrite
=
$sql
->
isWriteQuery
();
if
(
$isWrite
)
{
ChangedTablesTracker
::
recordQuery
(
$this
->
currentDomain
,
$sql
);
// Permit temporary table writes on replica connections, but require a writable
// master connection for writes to persistent tables.
if
(
$this
->
hasPermanentTable
(
$sql
)
)
{
$isPermWrite
=
true
;
$info
=
$this
->
getReadOnlyReason
();
if
(
$info
)
{
[
$reason
,
$source
]
=
$info
;
if
(
$source
===
'role'
)
{
throw
new
DBReadOnlyRoleError
(
$this
,
"Database is read-only: $reason"
);
}
else
{
throw
new
DBReadOnlyError
(
$this
,
"Database is read-only: $reason"
);
}
}
// DBConnRef uses QUERY_REPLICA_ROLE to enforce replica roles during query()
if
(
$this
->
flagsHolder
::
contains
(
$sql
->
getFlags
(),
self
::
QUERY_REPLICA_ROLE
)
)
{
throw
new
DBReadOnlyRoleError
(
$this
,
"Cannot write; target role is DB_REPLICA"
);
}
}
}
// Whether a silent retry attempt is left for recoverable connection loss errors
$retryLeft
=
!
$this
->
flagsHolder
::
contains
(
$flags
,
self
::
QUERY_NO_RETRY
);
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
do
{
// Start a DBO_TRX wrapper transaction as needed (throw an error on failure)
if
(
$this
->
beginIfImplied
(
$sql
,
$fname
,
$flags
)
)
{
// Since begin() was called, any connection loss was already handled
$retryLeft
=
false
;
}
// Send the query statement to the server and fetch any results.
$status
=
$this
->
attemptQuery
(
$sql
,
$fname
,
$isPermWrite
);
}
while
(
// An error occurred that can be recovered from via query retry
$this
->
flagsHolder
::
contains
(
$status
->
flags
,
self
::
ERR_RETRY_QUERY
)
&&
// The retry has not been exhausted (consume it now)
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
$retryLeft
&&
!(
$retryLeft
=
false
)
);
// Register creation and dropping of temporary tables
if
(
$status
->
res
)
{
$this
->
registerTempTables
(
$sql
);
}
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
return
$status
;
}
/**
* Query method wrapper handling profiling, logging, affected row count tracking, and
* automatic reconnections (without retry) on query failure due to connection loss
*
* Note that this does not handle DBO_TRX logic.
*
* This method handles profiling, debug logging, reconnection and the tracking of:
* - write callers
* - last write time
* - affected row count of the last write
* - whether writes occurred in a transaction
* - last successful query time (confirming that the connection was not dropped)
*
* @see doSingleStatementQuery()
*
* @param Query $sql SQL statement
* @param string $fname Name of the calling function
* @param bool $isPermWrite Whether it's a query writing to permanent tables
* @return QueryStatus statement result
* @throws DBUnexpectedError
*/
private
function
attemptQuery
(
$sql
,
string
$fname
,
bool
$isPermWrite
)
{
// Transaction attributes before issuing this query
$priorSessInfo
=
new
CriticalSessionInfo
(
$this
->
transactionManager
->
getTrxId
(),
$this
->
transactionManager
->
explicitTrxActive
(),
$this
->
transactionManager
->
pendingWriteCallers
(),
$this
->
transactionManager
->
pendingPreCommitCallbackCallers
(),
$this
->
sessionNamedLocks
,
$this
->
sessionTempTables
);
// Get the transaction-aware SQL string used for profiling
$prefix
=
(
$this
->
replicationReporter
->
getTopologyRole
()
===
self
::
ROLE_STREAMING_MASTER
)
?
'role-primary: '
:
''
;
// Start profile section
if
(
$sql
->
getCleanedSql
()
)
{
$generalizedSql
=
$sql
;
$ps
=
$this
->
profiler
?
(
$this
->
profiler
)(
$sql
->
getCleanedSql
()
)
:
null
;
}
else
{
$generalizedSql
=
new
GeneralizedSql
(
$sql
->
getSQL
(),
$prefix
);
$ps
=
$this
->
profiler
?
(
$this
->
profiler
)(
$generalizedSql
->
stringify
()
)
:
null
;
}
// Add agent and calling method comments to the SQL
$cStatement
=
$this
->
makeCommentedSql
(
$sql
->
getSQL
(),
$fname
);
$startTime
=
microtime
(
true
);
// Clear any overrides from a prior "query method". Note that this does not affect
// any such methods that are currently invoking query() itself since those query
// methods set these fields before returning.
$this
->
lastEmulatedAffectedRows
=
null
;
$this
->
lastEmulatedInsertId
=
null
;
$status
=
$this
->
doSingleStatementQuery
(
$cStatement
);
// End profile section
$endTime
=
microtime
(
true
);
$queryRuntime
=
max
(
$endTime
-
$startTime
,
0.0
);
unset
(
$ps
);
if
(
$status
->
res
!==
false
)
{
$this
->
lastPing
=
$endTime
;
}
$affectedRowCount
=
$status
->
rowsAffected
;
$returnedRowCount
=
$status
->
rowsReturned
;
$this
->
lastQueryAffectedRows
=
$affectedRowCount
;
if
(
$status
->
res
!==
false
)
{
if
(
$isPermWrite
)
{
$this
->
lastWriteTime
=
$startTime
;
if
(
$this
->
trxLevel
()
)
{
$this
->
transactionManager
->
transactionWritingIn
(
$this
->
getServerName
(),
$this
->
getDomainID
(),
$startTime
);
$this
->
transactionManager
->
updateTrxWriteQueryReport
(
$sql
->
getSQL
(),
$queryRuntime
,
$affectedRowCount
,
$fname
);
}
}
}
$this
->
transactionManager
->
recordQueryCompletion
(
$generalizedSql
,
$startTime
,
$isPermWrite
,
$isPermWrite
?
$affectedRowCount
:
$returnedRowCount
,
$this
->
getServerName
()
);
// Check if the query failed...
$status
->
flags
=
$this
->
handleErroredQuery
(
$status
,
$sql
,
$fname
,
$queryRuntime
,
$priorSessInfo
);
// Avoid the overhead of logging calls unless debug mode is enabled
if
(
$this
->
flagsHolder
->
getFlag
(
self
::
DBO_DEBUG
)
)
{
$this
->
logger
->
debug
(
"{method} [{runtime_ms}ms] {db_server}: {sql}"
,
$this
->
getLogContext
(
[
'method'
=>
$fname
,
'sql'
=>
$sql
->
getSQL
(),
'domain'
=>
$this
->
getDomainID
(),
'runtime_ms'
=>
round
(
$queryRuntime
*
1000
,
3
),
'db_log_category'
=>
'query'
]
)
);
}
return
$status
;
}
private
function
handleErroredQuery
(
QueryStatus
$status
,
$sql
,
$fname
,
$queryRuntime
,
$priorSessInfo
)
{
$errflags
=
self
::
ERR_NONE
;
$error
=
$status
->
message
;
$errno
=
$status
->
code
;
if
(
$status
->
res
!==
false
)
{
// Statement succeeded
return
$errflags
;
}
if
(
$this
->
isConnectionError
(
$errno
)
)
{
// Connection lost before or during the query...
// Determine how to proceed given the lost session state
$connLossFlag
=
$this
->
assessConnectionLoss
(
$sql
->
getVerb
(),
$queryRuntime
,
$priorSessInfo
);
// Update session state tracking and try to reestablish a connection
$reconnected
=
$this
->
replaceLostConnection
(
$errno
,
__METHOD__
);
// Check if important server-side session-level state was lost
if
(
$connLossFlag
>=
self
::
ERR_ABORT_SESSION
)
{
$ex
=
$this
->
getQueryException
(
$error
,
$errno
,
$sql
->
getSQL
(),
$fname
);
$this
->
transactionManager
->
setSessionError
(
$ex
);
}
// Check if important server-side transaction-level state was lost
if
(
$connLossFlag
>=
self
::
ERR_ABORT_TRX
)
{
$ex
=
$this
->
getQueryException
(
$error
,
$errno
,
$sql
->
getSQL
(),
$fname
);
$this
->
transactionManager
->
setTransactionError
(
$ex
);
}
// Check if the query should be retried (having made the reconnection attempt)
if
(
$connLossFlag
===
self
::
ERR_RETRY_QUERY
)
{
$errflags
|=
(
$reconnected
?
self
::
ERR_RETRY_QUERY
:
self
::
ERR_ABORT_QUERY
);
}
else
{
$errflags
|=
$connLossFlag
;
}
}
elseif
(
$this
->
isKnownStatementRollbackError
(
$errno
)
)
{
// Query error triggered a server-side statement-only rollback...
$errflags
|=
self
::
ERR_ABORT_QUERY
;
if
(
$this
->
trxLevel
()
)
{
// Allow legacy callers to ignore such errors via QUERY_IGNORE_DBO_TRX and
// try/catch. However, a deprecation notice will be logged on the next query.
$cause
=
[
$error
,
$errno
,
$fname
];
$this
->
transactionManager
->
setTrxStatusIgnoredCause
(
$cause
);
}
}
elseif
(
$this
->
trxLevel
()
)
{
// Some other error occurred during the query, within a transaction...
// Server-side handling of errors during transactions varies widely depending on
// the RDBMS type and configuration. There are several possible results: (a) the
// whole transaction is rolled back, (b) only the queries after BEGIN are rolled
// back, (c) the transaction is marked as "aborted" and a ROLLBACK is required
// before other queries are permitted. For compatibility reasons, pessimistically
// require a ROLLBACK query (not using SAVEPOINT) before allowing other queries.
$ex
=
$this
->
getQueryException
(
$error
,
$errno
,
$sql
->
getSQL
(),
$fname
);
$this
->
transactionManager
->
setTransactionError
(
$ex
);
$errflags
|=
self
::
ERR_ABORT_TRX
;
}
else
{
// Some other error occurred during the query, without a transaction...
$errflags
|=
self
::
ERR_ABORT_QUERY
;
}
return
$errflags
;
}
/**
* @param string $sql
* @param string $fname
* @return string
*/
private
function
makeCommentedSql
(
$sql
,
$fname
):
string
{
// Add trace comment to the begin of the sql string, right after the operator.
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
// NOTE: Don't add varying ids such as request id or session id to the comment.
// It would break aggregation of similar queries in analysis tools (see T193050#7512149)
$encName
=
preg_replace
(
'/[
\x
00-
\x
1F
\/
]/'
,
'-'
,
"$fname {$this->agent}"
);
return
preg_replace
(
'/
\s
|$/'
,
" /* $encName */ "
,
$sql
,
1
);
}
/**
* Start an implicit transaction if DBO_TRX is enabled and no transaction is active
*
* @param Query $sql SQL statement
* @param string $fname
* @param int $flags Bit field of ISQLPlatform::QUERY_* constants
* @return bool Whether an implicit transaction was started
* @throws DBError
*/
private
function
beginIfImplied
(
$sql
,
$fname
,
$flags
)
{
if
(
!
$this
->
trxLevel
()
&&
$this
->
flagsHolder
->
hasApplicableImplicitTrxFlag
(
$flags
)
)
{
if
(
$this
->
platform
->
isTransactableQuery
(
$sql
)
)
{
$this
->
begin
(
__METHOD__
.
" ($fname)"
,
self
::
TRANSACTION_INTERNAL
);
$this
->
transactionManager
->
turnOnAutomatic
();
return
true
;
}
}
return
false
;
}
/**
* Check if callers outside of Database can run the given query given the session state
*
* In order to keep the DB handle's session state tracking in sync, certain queries
* like "USE", "BEGIN", "COMMIT", and "ROLLBACK" must not be issued directly from
* outside callers. Such commands should only be issued through dedicated methods
* like selectDomain(), begin(), commit(), and rollback(), respectively.
*
* This also checks if the session state tracking was corrupted by a prior exception.
*
* @param string $verb
* @param string $fname
* @throws DBUnexpectedError
* @throws DBTransactionStateError
*/
private
function
assertQueryIsCurrentlyAllowed
(
string
$verb
,
string
$fname
)
{
if
(
$verb
===
'USE'
)
{
throw
new
DBUnexpectedError
(
$this
,
"Got USE query; use selectDomain() instead"
);
}
if
(
$verb
===
'ROLLBACK'
)
{
// Whole transaction rollback is used for recovery
// @TODO: T269161; prevent "BEGIN"/"COMMIT"/"ROLLBACK" from outside callers
return
;
}
if
(
$this
->
csmError
)
{
throw
new
DBTransactionStateError
(
$this
,
"Cannot execute query from $fname while session state is out of sync"
,
[],
$this
->
csmError
);
}
$this
->
transactionManager
->
assertSessionStatus
(
$this
,
$fname
);
if
(
$verb
!==
'ROLLBACK TO SAVEPOINT'
)
{
$this
->
transactionManager
->
assertTransactionStatus
(
$this
,
$this
->
deprecationLogger
,
$fname
);
}
}
/**
* Determine how to handle a connection lost discovered during a query attempt
*
* This checks if explicit transactions, pending transaction writes, and important
* session-level state (locks, temp tables) was lost. Point-in-time read snapshot loss
* is considered acceptable for DBO_TRX logic.
*
* If state was lost, but that loss was discovered during a ROLLBACK that would have
* destroyed that state anyway, treat the error as recoverable.
*
* @param string $verb SQL query verb
* @param float $walltime How many seconds passes while attempting the query
* @param CriticalSessionInfo $priorSessInfo Session state just before the query
* @return int Recovery approach. One of the following ERR_* class constants:
* - Database::ERR_RETRY_QUERY: reconnect silently, retry query
* - Database::ERR_ABORT_QUERY: reconnect silently, do not retry query
* - Database::ERR_ABORT_TRX: reconnect, throw error, enforce transaction rollback
* - Database::ERR_ABORT_SESSION: reconnect, throw error, enforce session rollback
*/
private
function
assessConnectionLoss
(
string
$verb
,
float
$walltime
,
CriticalSessionInfo
$priorSessInfo
)
{
if
(
$walltime
<
self
::
DROPPED_CONN_BLAME_THRESHOLD_SEC
)
{
// Query failed quickly; the connection was probably lost before the query was sent
$res
=
self
::
ERR_RETRY_QUERY
;
}
else
{
// Query took a long time; the connection was probably lost during query execution
$res
=
self
::
ERR_ABORT_QUERY
;
}
// List of problems causing session/transaction state corruption
$blockers
=
[];
// Loss of named locks breaks future callers relying on those locks for critical sections
foreach
(
$priorSessInfo
->
namedLocks
as
$lockName
=>
$lockInfo
)
{
if
(
$lockInfo
[
'trxId'
]
&&
$lockInfo
[
'trxId'
]
===
$priorSessInfo
->
trxId
)
{
// Treat lost locks acquired during the lost transaction as a transaction state
// problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
// rollback automatically triggered server-side.
if
(
$verb
!==
'ROLLBACK'
)
{
$res
=
max
(
$res
,
self
::
ERR_ABORT_TRX
);
$blockers
[]
=
"named lock '$lockName'"
;
}
}
else
{
// Treat lost locks acquired either during prior transactions or during no
// transaction as a session state problem.
$res
=
max
(
$res
,
self
::
ERR_ABORT_SESSION
);
$blockers
[]
=
"named lock '$lockName'"
;
}
}
// Loss of temp tables breaks future callers relying on those tables for queries
foreach
(
$priorSessInfo
->
tempTables
as
$domainTempTables
)
{
foreach
(
$domainTempTables
as
$tableName
=>
$tableInfo
)
{
if
(
$tableInfo
->
trxId
&&
$tableInfo
->
trxId
===
$priorSessInfo
->
trxId
)
{
// Treat lost temp tables created during the lost transaction as a
// transaction state problem. Connection loss on ROLLBACK (non-SAVEPOINT)
// is tolerable since rollback automatically triggered server-side.
if
(
$verb
!==
'ROLLBACK'
)
{
$res
=
max
(
$res
,
self
::
ERR_ABORT_TRX
);
$blockers
[]
=
"temp table '$tableName'"
;
}
}
else
{
// Treat lost temp tables created either during prior transactions or during
// no transaction as a session state problem.
$res
=
max
(
$res
,
self
::
ERR_ABORT_SESSION
);
$blockers
[]
=
"temp table '$tableName'"
;
}
}
}
// Loss of transaction writes breaks future callers and DBO_TRX logic relying on those
// writes to be atomic and still pending. Connection loss on ROLLBACK (non-SAVEPOINT) is
// tolerable since rollback automatically triggered server-side.
if
(
$priorSessInfo
->
trxWriteCallers
&&
$verb
!==
'ROLLBACK'
)
{
$res
=
max
(
$res
,
self
::
ERR_ABORT_TRX
);
$blockers
[]
=
'uncommitted writes'
;
}
if
(
$priorSessInfo
->
trxPreCommitCbCallers
&&
$verb
!==
'ROLLBACK'
)
{
$res
=
max
(
$res
,
self
::
ERR_ABORT_TRX
);
$blockers
[]
=
'pre-commit callbacks'
;
}
if
(
$priorSessInfo
->
trxExplicit
&&
$verb
!==
'ROLLBACK'
&&
$verb
!==
'COMMIT'
)
{
// Transaction automatically rolled back, breaking the expectations of callers
// relying on the continued existence of that transaction for things like atomic
// writes, serializability, or reads from the same point-in-time snapshot. If the
// connection loss occured on ROLLBACK (non-SAVEPOINT) or COMMIT, then we do not
// need to mark the transaction state as corrupt, since no transaction would still
// be open even if the query did succeed (T127428).
$res
=
max
(
$res
,
self
::
ERR_ABORT_TRX
);
$blockers
[]
=
'explicit transaction'
;
}
if
(
$blockers
)
{
$this
->
logger
->
warning
(
"cannot reconnect to {db_server} silently: {error}"
,
$this
->
getLogContext
(
[
'error'
=>
'session state loss ('
.
implode
(
', '
,
$blockers
)
.
')'
,
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'connection'
]
)
);
}
return
$res
;
}
/**
* Clean things up after session (and thus transaction) loss before reconnect
*/
private
function
handleSessionLossPreconnect
()
{
// Clean up tracking of session-level things...
// https://mariadb.com/kb/en/create-table/#create-temporary-table
// https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
$this
->
sessionTempTables
=
[];
// https://mariadb.com/kb/en/get_lock/
// https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
$this
->
sessionNamedLocks
=
[];
// Session loss implies transaction loss (T67263)
$this
->
transactionManager
->
onSessionLoss
(
$this
);
// Clear additional subclass fields
$this
->
doHandleSessionLossPreconnect
();
}
/**
* Reset any additional subclass trx* and session* fields
*/
protected
function
doHandleSessionLossPreconnect
()
{
// no-op
}
/**
* Checks whether the cause of the error is detected to be a timeout.
*
* It returns false by default, and not all engines support detecting this yet.
* If this returns false, it will be treated as a generic query error.
*
* @param int|string $errno Error number
* @return bool
* @since 1.39
*/
protected
function
isQueryTimeoutError
(
$errno
)
{
return
false
;
}
/**
* Report a query error
*
* If $ignore is set, emit a DEBUG level log entry and continue,
* otherwise, emit an ERROR level log entry and throw an exception.
*
* @param string $error
* @param int|string $errno
* @param string $sql
* @param string $fname
* @param bool $ignore Whether to just log an error rather than throw an exception
* @throws DBQueryError
*/
public
function
reportQueryError
(
$error
,
$errno
,
$sql
,
$fname
,
$ignore
=
false
)
{
if
(
$ignore
)
{
$this
->
logger
->
debug
(
"SQL ERROR (ignored): $error"
,
[
'db_log_category'
=>
'query'
]
);
}
else
{
throw
$this
->
getQueryExceptionAndLog
(
$error
,
$errno
,
$sql
,
$fname
);
}
}
/**
* @param string $error
* @param string|int $errno
* @param string $sql
* @param string $fname
* @return DBError
*/
private
function
getQueryExceptionAndLog
(
$error
,
$errno
,
$sql
,
$fname
)
{
// Information that instances of the same problem have in common should
// not be normalized (T255202).
$this
->
logger
->
error
(
"Error $errno from $fname, {error} {sql1line} {db_server}"
,
$this
->
getLogContext
(
[
'method'
=>
__METHOD__
,
'errno'
=>
$errno
,
'error'
=>
$error
,
'sql1line'
=>
mb_substr
(
str_replace
(
"
\n
"
,
"
\\
n"
,
$sql
),
0
,
5
*
1024
),
'fname'
=>
$fname
,
'db_log_category'
=>
'query'
,
'exception'
=>
new
RuntimeException
()
]
)
);
return
$this
->
getQueryException
(
$error
,
$errno
,
$sql
,
$fname
);
}
/**
* @param string $error
* @param string|int $errno
* @param string $sql
* @param string $fname
* @return DBError
*/
private
function
getQueryException
(
$error
,
$errno
,
$sql
,
$fname
)
{
if
(
$this
->
isQueryTimeoutError
(
$errno
)
)
{
return
new
DBQueryTimeoutError
(
$this
,
$error
,
$errno
,
$sql
,
$fname
);
}
elseif
(
$this
->
isConnectionError
(
$errno
)
)
{
return
new
DBQueryDisconnectedError
(
$this
,
$error
,
$errno
,
$sql
,
$fname
);
}
else
{
return
new
DBQueryError
(
$this
,
$error
,
$errno
,
$sql
,
$fname
);
}
}
/**
* @param string $error
* @return DBConnectionError
*/
final
protected
function
newExceptionAfterConnectError
(
$error
)
{
// Connection was not fully initialized and is not safe for use.
// Stash any error associated with the handle before destroying it.
$this
->
lastConnectError
=
$error
;
$this
->
conn
=
null
;
$this
->
logger
->
error
(
"Error connecting to {db_server} as user {db_user}: {error}"
,
$this
->
getLogContext
(
[
'error'
=>
$error
,
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'connection'
,
]
)
);
return
new
DBConnectionError
(
$this
,
$error
);
}
/**
* Get a SelectQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return SelectQueryBuilder
*/
public
function
newSelectQueryBuilder
():
SelectQueryBuilder
{
return
new
SelectQueryBuilder
(
$this
);
}
/**
* Get a UnionQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return UnionQueryBuilder
*/
public
function
newUnionQueryBuilder
():
UnionQueryBuilder
{
return
new
UnionQueryBuilder
(
$this
);
}
/**
* Get an UpdateQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return UpdateQueryBuilder
*/
public
function
newUpdateQueryBuilder
():
UpdateQueryBuilder
{
return
new
UpdateQueryBuilder
(
$this
);
}
/**
* Get a DeleteQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return DeleteQueryBuilder
*/
public
function
newDeleteQueryBuilder
():
DeleteQueryBuilder
{
return
new
DeleteQueryBuilder
(
$this
);
}
/**
* Get a InsertQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return InsertQueryBuilder
*/
public
function
newInsertQueryBuilder
():
InsertQueryBuilder
{
return
new
InsertQueryBuilder
(
$this
);
}
/**
* Get a ReplaceQueryBuilder bound to this connection. This is overridden by
* DBConnRef.
*
* @return ReplaceQueryBuilder
*/
public
function
newReplaceQueryBuilder
():
ReplaceQueryBuilder
{
return
new
ReplaceQueryBuilder
(
$this
);
}
public
function
selectField
(
$tables
,
$var
,
$cond
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
if
(
$var
===
'*'
)
{
throw
new
DBUnexpectedError
(
$this
,
"Cannot use a * field"
);
}
elseif
(
is_array
(
$var
)
&&
count
(
$var
)
!==
1
)
{
throw
new
DBUnexpectedError
(
$this
,
'Cannot use more than one field'
);
}
$options
=
$this
->
platform
->
normalizeOptions
(
$options
);
$options
[
'LIMIT'
]
=
1
;
$res
=
$this
->
select
(
$tables
,
$var
,
$cond
,
$fname
,
$options
,
$join_conds
);
if
(
$res
===
false
)
{
throw
new
DBUnexpectedError
(
$this
,
"Got false from select()"
);
}
$row
=
$res
->
fetchRow
();
if
(
$row
===
false
)
{
return
false
;
}
return
reset
(
$row
);
}
public
function
selectFieldValues
(
$tables
,
$var
,
$cond
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
):
array
{
if
(
$var
===
'*'
)
{
throw
new
DBUnexpectedError
(
$this
,
"Cannot use a * field"
);
}
elseif
(
!
is_string
(
$var
)
)
{
throw
new
DBUnexpectedError
(
$this
,
"Cannot use an array of fields"
);
}
$options
=
$this
->
platform
->
normalizeOptions
(
$options
);
$res
=
$this
->
select
(
$tables
,
[
'value'
=>
$var
],
$cond
,
$fname
,
$options
,
$join_conds
);
if
(
$res
===
false
)
{
throw
new
DBUnexpectedError
(
$this
,
"Got false from select()"
);
}
$values
=
[];
foreach
(
$res
as
$row
)
{
$values
[]
=
$row
->
value
;
}
return
$values
;
}
public
function
select
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
$options
=
(
array
)
$options
;
// Don't turn this into using platform directly, DatabaseMySQL overrides this.
$sql
=
$this
->
selectSQLText
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
);
// Treat SELECT queries with FOR UPDATE as writes. This matches
// how MySQL enforces read_only (FOR SHARE and LOCK IN SHADE MODE are allowed).
$flags
=
in_array
(
'FOR UPDATE'
,
$options
,
true
)
?
self
::
QUERY_CHANGE_ROWS
:
self
::
QUERY_CHANGE_NONE
;
$query
=
new
Query
(
$sql
,
$flags
,
'SELECT'
);
return
$this
->
query
(
$query
,
$fname
);
}
public
function
selectRow
(
$tables
,
$vars
,
$conds
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
$options
=
(
array
)
$options
;
$options
[
'LIMIT'
]
=
1
;
$res
=
$this
->
select
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
);
if
(
$res
===
false
)
{
throw
new
DBUnexpectedError
(
$this
,
"Got false from select()"
);
}
if
(
!
$res
->
numRows
()
)
{
return
false
;
}
return
$res
->
fetchObject
();
}
/**
* @inheritDoc
*/
public
function
estimateRowCount
(
$tables
,
$var
=
'*'
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
):
int
{
$conds
=
$this
->
platform
->
normalizeConditions
(
$conds
,
$fname
);
$column
=
$this
->
platform
->
extractSingleFieldFromList
(
$var
);
if
(
is_string
(
$column
)
&&
!
in_array
(
$column
,
[
'*'
,
'1'
]
)
)
{
$conds
[]
=
"$column IS NOT NULL"
;
}
$res
=
$this
->
select
(
$tables
,
[
'rowcount'
=>
'COUNT(*)'
],
$conds
,
$fname
,
$options
,
$join_conds
);
$row
=
$res
?
$res
->
fetchRow
()
:
[];
return
isset
(
$row
[
'rowcount'
]
)
?
(
int
)
$row
[
'rowcount'
]
:
0
;
}
public
function
selectRowCount
(
$tables
,
$var
=
'*'
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
):
int
{
$conds
=
$this
->
platform
->
normalizeConditions
(
$conds
,
$fname
);
$column
=
$this
->
platform
->
extractSingleFieldFromList
(
$var
);
if
(
is_string
(
$column
)
&&
!
in_array
(
$column
,
[
'*'
,
'1'
]
)
)
{
$conds
[]
=
"$column IS NOT NULL"
;
}
if
(
in_array
(
'DISTINCT'
,
(
array
)
$options
)
)
{
if
(
$column
===
null
)
{
throw
new
DBUnexpectedError
(
$this
,
'$var cannot be empty when the DISTINCT option is given'
);
}
$innerVar
=
$column
;
}
else
{
$innerVar
=
'1'
;
}
$res
=
$this
->
select
(
[
'tmp_count'
=>
$this
->
platform
->
buildSelectSubquery
(
$tables
,
$innerVar
,
$conds
,
$fname
,
$options
,
$join_conds
)
],
[
'rowcount'
=>
'COUNT(*)'
],
[],
$fname
);
$row
=
$res
?
$res
->
fetchRow
()
:
[];
return
isset
(
$row
[
'rowcount'
]
)
?
(
int
)
$row
[
'rowcount'
]
:
0
;
}
public
function
lockForUpdate
(
$table
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
if
(
!
$this
->
trxLevel
()
&&
!
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
)
{
throw
new
DBUnexpectedError
(
$this
,
__METHOD__
.
': no transaction is active nor is DBO_TRX set'
);
}
$options
=
(
array
)
$options
;
$options
[]
=
'FOR UPDATE'
;
return
$this
->
selectRowCount
(
$table
,
'*'
,
$conds
,
$fname
,
$options
,
$join_conds
);
}
public
function
fieldExists
(
$table
,
$field
,
$fname
=
__METHOD__
)
{
$info
=
$this
->
fieldInfo
(
$table
,
$field
);
return
(
bool
)
$info
;
}
abstract
public
function
tableExists
(
$table
,
$fname
=
__METHOD__
);
public
function
indexExists
(
$table
,
$index
,
$fname
=
__METHOD__
)
{
$info
=
$this
->
indexInfo
(
$table
,
$index
,
$fname
);
return
(
bool
)
$info
;
}
public
function
indexUnique
(
$table
,
$index
,
$fname
=
__METHOD__
)
{
$info
=
$this
->
indexInfo
(
$table
,
$index
,
$fname
);
return
$info
?
$info
[
'unique'
]
:
null
;
}
/**
* Get information about an index into an object
*
* @param string $table The unqualified name of a table
* @param string $index Index name
* @param string $fname Calling function name
* @return array<string,mixed>|false Index info map; false if it does not exist
* @phan-return array{unique:bool}|false
*/
abstract
public
function
indexInfo
(
$table
,
$index
,
$fname
=
__METHOD__
);
public
function
insert
(
$table
,
$rows
,
$fname
=
__METHOD__
,
$options
=
[]
)
{
$query
=
$this
->
platform
->
dispatchingInsertSqlText
(
$table
,
$rows
,
$options
);
if
(
!
$query
)
{
return
true
;
}
$this
->
query
(
$query
,
$fname
);
if
(
$this
->
strictWarnings
)
{
$this
->
checkInsertWarnings
(
$query
,
$fname
);
}
return
true
;
}
/**
* Check for warnings after performing an INSERT query, and throw exceptions
* if necessary.
*
* @param Query $query
* @param string $fname
* @return void
*/
protected
function
checkInsertWarnings
(
Query
$query
,
$fname
)
{
}
public
function
update
(
$table
,
$set
,
$conds
,
$fname
=
__METHOD__
,
$options
=
[]
)
{
$query
=
$this
->
platform
->
updateSqlText
(
$table
,
$set
,
$conds
,
$options
);
$this
->
query
(
$query
,
$fname
);
return
true
;
}
public
function
databasesAreIndependent
()
{
return
false
;
}
final
public
function
selectDomain
(
$domain
)
{
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
try
{
$this
->
doSelectDomain
(
DatabaseDomain
::
newFromId
(
$domain
)
);
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
throw
$e
;
}
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
}
/**
* @param DatabaseDomain $domain
* @throws DBConnectionError
* @throws DBError
* @since 1.32
*/
protected
function
doSelectDomain
(
DatabaseDomain
$domain
)
{
$this
->
currentDomain
=
$domain
;
$this
->
platform
->
setCurrentDomain
(
$this
->
currentDomain
);
}
public
function
getDBname
()
{
return
$this
->
currentDomain
->
getDatabase
();
}
public
function
getServer
()
{
return
$this
->
connectionParams
[
self
::
CONN_HOST
]
??
null
;
}
public
function
getServerName
()
{
return
$this
->
serverName
??
$this
->
getServer
()
??
'unknown'
;
}
public
function
addQuotes
(
$s
)
{
if
(
$s
instanceof
RawSQLValue
)
{
return
$s
->
toSql
();
}
if
(
$s
instanceof
Blob
)
{
$s
=
$s
->
fetch
();
}
if
(
$s
===
null
)
{
return
'NULL'
;
}
elseif
(
is_bool
(
$s
)
)
{
return
(
string
)(
int
)
$s
;
}
elseif
(
is_int
(
$s
)
)
{
return
(
string
)
$s
;
}
else
{
return
"'"
.
$this
->
strencode
(
$s
)
.
"'"
;
}
}
public
function
expr
(
string
$field
,
string
$op
,
$value
):
Expression
{
return
new
Expression
(
$field
,
$op
,
$value
);
}
public
function
andExpr
(
array
$conds
):
AndExpressionGroup
{
return
AndExpressionGroup
::
newFromArray
(
$conds
);
}
public
function
orExpr
(
array
$conds
):
OrExpressionGroup
{
return
OrExpressionGroup
::
newFromArray
(
$conds
);
}
public
function
replace
(
$table
,
$uniqueKeys
,
$rows
,
$fname
=
__METHOD__
)
{
$uniqueKey
=
$this
->
platform
->
normalizeUpsertParams
(
$uniqueKeys
,
$rows
);
if
(
!
$rows
)
{
return
;
}
$affectedRowCount
=
0
;
$insertId
=
null
;
$this
->
startAtomic
(
$fname
,
self
::
ATOMIC_CANCELABLE
);
try
{
foreach
(
$rows
as
$row
)
{
// Delete any conflicting rows (including ones inserted from $rows)
$query
=
$this
->
platform
->
deleteSqlText
(
$table
,
[
$this
->
platform
->
makeKeyCollisionCondition
(
[
$row
],
$uniqueKey
)
]
);
$this
->
query
(
$query
,
$fname
);
// Insert the new row
$query
=
$this
->
platform
->
dispatchingInsertSqlText
(
$table
,
$row
,
[]
);
$this
->
query
(
$query
,
$fname
);
$affectedRowCount
+=
$this
->
lastQueryAffectedRows
;
$insertId
=
$insertId
?:
$this
->
lastQueryInsertId
;
}
$this
->
endAtomic
(
$fname
);
}
catch
(
DBError
$e
)
{
$this
->
cancelAtomic
(
$fname
);
throw
$e
;
}
$this
->
lastEmulatedAffectedRows
=
$affectedRowCount
;
$this
->
lastEmulatedInsertId
=
$insertId
;
}
public
function
upsert
(
$table
,
array
$rows
,
$uniqueKeys
,
array
$set
,
$fname
=
__METHOD__
)
{
$uniqueKey
=
$this
->
platform
->
normalizeUpsertParams
(
$uniqueKeys
,
$rows
);
if
(
!
$rows
)
{
return
true
;
}
$this
->
platform
->
assertValidUpsertSetArray
(
$set
,
$uniqueKey
,
$rows
);
$encTable
=
$this
->
tableName
(
$table
);
$sqlColumnAssignments
=
$this
->
makeList
(
$set
,
self
::
LIST_SET
);
// Get any AUTO_INCREMENT/SERIAL column for this table so we can set insertId()
$autoIncrementColumn
=
$this
->
getInsertIdColumnForUpsert
(
$table
);
// Check if there is a SQL assignment expression in $set (as generated by SQLPlatform::buildExcludedValue)
$useWith
=
(
bool
)
array_filter
(
$set
,
static
function
(
$v
,
$k
)
{
return
$v
instanceof
RawSQLValue
||
is_int
(
$k
);
},
ARRAY_FILTER_USE_BOTH
);
// Subclasses might need explicit type casting within "WITH...AS (VALUES ...)"
// so that these CTE rows can be referenced within the SET clause assigments.
$typeByColumn
=
$useWith
?
$this
->
getValueTypesForWithClause
(
$table
)
:
[];
$first
=
true
;
$affectedRowCount
=
0
;
$insertId
=
null
;
$this
->
startAtomic
(
$fname
,
self
::
ATOMIC_CANCELABLE
);
try
{
foreach
(
$rows
as
$row
)
{
// Update any existing conflicting row (including ones inserted from $rows)
[
$sqlColumns
,
$sqlTuples
,
$sqlVals
]
=
$this
->
platform
->
makeInsertLists
(
[
$row
],
'__'
,
$typeByColumn
);
$sqlConditions
=
$this
->
platform
->
makeKeyCollisionCondition
(
[
$row
],
$uniqueKey
);
$query
=
new
Query
(
(
$useWith
?
"WITH __VALS ($sqlVals) AS (VALUES $sqlTuples) "
:
""
)
.
"UPDATE $encTable SET $sqlColumnAssignments "
.
"WHERE ($sqlConditions)"
,
self
::
QUERY_CHANGE_ROWS
,
'UPDATE'
,
$table
);
$this
->
query
(
$query
,
$fname
);
$rowsUpdated
=
$this
->
lastQueryAffectedRows
;
$affectedRowCount
+=
$rowsUpdated
;
if
(
$rowsUpdated
>
0
)
{
// Conflicting row found and updated
if
(
$first
&&
$autoIncrementColumn
!==
null
)
{
// @TODO: use "RETURNING" instead (when supported by SQLite)
$query
=
new
Query
(
"SELECT $autoIncrementColumn AS id FROM $encTable "
.
"WHERE ($sqlConditions)"
,
self
::
QUERY_CHANGE_NONE
,
'SELECT'
);
$sRes
=
$this
->
query
(
$query
,
$fname
,
self
::
QUERY_CHANGE_ROWS
);
$insertId
=
(
int
)
$sRes
->
fetchRow
()[
'id'
];
}
}
else
{
// No conflicting row found
$query
=
new
Query
(
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples"
,
self
::
QUERY_CHANGE_ROWS
,
'INSERT'
,
$table
);
$this
->
query
(
$query
,
$fname
);
$affectedRowCount
+=
$this
->
lastQueryAffectedRows
;
}
$first
=
false
;
}
$this
->
endAtomic
(
$fname
);
}
catch
(
DBError
$e
)
{
$this
->
cancelAtomic
(
$fname
);
throw
$e
;
}
$this
->
lastEmulatedAffectedRows
=
$affectedRowCount
;
$this
->
lastEmulatedInsertId
=
$insertId
;
return
true
;
}
/**
* @param string $table The unqualified name of a table
* @return string|null The AUTO_INCREMENT/SERIAL column; null if not needed
*/
protected
function
getInsertIdColumnForUpsert
(
$table
)
{
return
null
;
}
/**
* @param string $table The unqualified name of a table
* @return array<string,string> Map of (column => type); [] if not needed
*/
protected
function
getValueTypesForWithClause
(
$table
)
{
return
[];
}
public
function
deleteJoin
(
$delTable
,
$joinTable
,
$delVar
,
$joinVar
,
$conds
,
$fname
=
__METHOD__
)
{
$sql
=
$this
->
platform
->
deleteJoinSqlText
(
$delTable
,
$joinTable
,
$delVar
,
$joinVar
,
$conds
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_ROWS
,
'DELETE'
,
$delTable
);
$this
->
query
(
$query
,
$fname
);
}
public
function
delete
(
$table
,
$conds
,
$fname
=
__METHOD__
)
{
$this
->
query
(
$this
->
platform
->
deleteSqlText
(
$table
,
$conds
),
$fname
);
return
true
;
}
final
public
function
insertSelect
(
$destTable
,
$srcTable
,
$varMap
,
$conds
,
$fname
=
__METHOD__
,
$insertOptions
=
[],
$selectOptions
=
[],
$selectJoinConds
=
[]
)
{
static
$hints
=
[
'NO_AUTO_COLUMNS'
];
$insertOptions
=
$this
->
platform
->
normalizeOptions
(
$insertOptions
);
$selectOptions
=
$this
->
platform
->
normalizeOptions
(
$selectOptions
);
if
(
$this
->
cliMode
&&
$this
->
isInsertSelectSafe
(
$insertOptions
,
$selectOptions
,
$fname
)
)
{
// For massive migrations with downtime, we don't want to select everything
// into memory and OOM, so do all this native on the server side if possible.
$this
->
doInsertSelectNative
(
$destTable
,
$srcTable
,
$varMap
,
$conds
,
$fname
,
array_diff
(
$insertOptions
,
$hints
),
$selectOptions
,
$selectJoinConds
);
}
else
{
$this
->
doInsertSelectGeneric
(
$destTable
,
$srcTable
,
$varMap
,
$conds
,
$fname
,
array_diff
(
$insertOptions
,
$hints
),
$selectOptions
,
$selectJoinConds
);
}
return
true
;
}
/**
* @param array $insertOptions
* @param array $selectOptions
* @param string $fname
* @return bool Whether an INSERT SELECT with these options will be replication safe
* @since 1.31
*/
protected
function
isInsertSelectSafe
(
array
$insertOptions
,
array
$selectOptions
,
$fname
)
{
return
true
;
}
/**
* Implementation of insertSelect() based on select() and insert()
*
* @see IDatabase::insertSelect()
* @param string $destTable Unqualified name of destination table
* @param string|array $srcTable Unqualified name of source table
* @param array $varMap
* @param array $conds
* @param string $fname
* @param array $insertOptions
* @param array $selectOptions
* @param array $selectJoinConds
* @since 1.35
*/
private
function
doInsertSelectGeneric
(
$destTable
,
$srcTable
,
array
$varMap
,
$conds
,
$fname
,
array
$insertOptions
,
array
$selectOptions
,
$selectJoinConds
)
{
// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
// on only the primary DB (without needing row-based-replication). It also makes it easy to
// know how big the INSERT is going to be.
$fields
=
[];
foreach
(
$varMap
as
$dstColumn
=>
$sourceColumnOrSql
)
{
$fields
[]
=
$this
->
platform
->
fieldNameWithAlias
(
$sourceColumnOrSql
,
$dstColumn
);
}
$res
=
$this
->
select
(
$srcTable
,
implode
(
','
,
$fields
),
$conds
,
$fname
,
array_merge
(
$selectOptions
,
[
'FOR UPDATE'
]
),
$selectJoinConds
);
$affectedRowCount
=
0
;
$insertId
=
null
;
if
(
$res
)
{
$this
->
startAtomic
(
$fname
,
self
::
ATOMIC_CANCELABLE
);
try
{
$rows
=
[];
foreach
(
$res
as
$row
)
{
$rows
[]
=
(
array
)
$row
;
}
// Avoid inserts that are too huge
$rowBatches
=
array_chunk
(
$rows
,
$this
->
nonNativeInsertSelectBatchSize
);
foreach
(
$rowBatches
as
$rows
)
{
$query
=
$this
->
platform
->
dispatchingInsertSqlText
(
$destTable
,
$rows
,
$insertOptions
);
$this
->
query
(
$query
,
$fname
);
$affectedRowCount
+=
$this
->
lastQueryAffectedRows
;
$insertId
=
$insertId
?:
$this
->
lastQueryInsertId
;
}
$this
->
endAtomic
(
$fname
);
}
catch
(
DBError
$e
)
{
$this
->
cancelAtomic
(
$fname
);
throw
$e
;
}
}
$this
->
lastEmulatedAffectedRows
=
$affectedRowCount
;
$this
->
lastEmulatedInsertId
=
$insertId
;
}
/**
* Native server-side implementation of insertSelect() for situations where
* we don't want to select everything into memory
*
* @see IDatabase::insertSelect()
* @param string $destTable The unqualified name of destination table
* @param string|array $srcTable The unqualified name of source table
* @param array $varMap
* @param array $conds
* @param string $fname
* @param array $insertOptions
* @param array $selectOptions
* @param array $selectJoinConds
* @since 1.35
*/
protected
function
doInsertSelectNative
(
$destTable
,
$srcTable
,
array
$varMap
,
$conds
,
$fname
,
array
$insertOptions
,
array
$selectOptions
,
$selectJoinConds
)
{
$sql
=
$this
->
platform
->
insertSelectNativeSqlText
(
$destTable
,
$srcTable
,
$varMap
,
$conds
,
$fname
,
$insertOptions
,
$selectOptions
,
$selectJoinConds
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_ROWS
,
'INSERT'
,
$destTable
);
$this
->
query
(
$query
,
$fname
);
}
/**
* Do not use this method outside of Database/DBError classes
*
* @param int|string $errno
* @return bool Whether the given query error was a connection drop
* @since 1.38
*/
protected
function
isConnectionError
(
$errno
)
{
return
false
;
}
/**
* @param int|string $errno
* @return bool Whether it is known that the last query error only caused statement rollback
* @note This is for backwards compatibility for callers catching DBError exceptions in
* order to ignore problems like duplicate key errors or foreign key violations
* @since 1.39
*/
protected
function
isKnownStatementRollbackError
(
$errno
)
{
return
false
;
// don't know; it could have caused a transaction rollback
}
/**
* @inheritDoc
*/
public
function
serverIsReadOnly
()
{
return
false
;
}
final
public
function
onTransactionResolution
(
callable
$callback
,
$fname
=
__METHOD__
)
{
$this
->
transactionManager
->
onTransactionResolution
(
$this
,
$callback
,
$fname
);
}
final
public
function
onTransactionCommitOrIdle
(
callable
$callback
,
$fname
=
__METHOD__
)
{
if
(
!
$this
->
trxLevel
()
&&
$this
->
getTransactionRoundId
()
)
{
// This DB handle is set to participate in LoadBalancer transaction rounds and
// an explicit transaction round is active. Start an implicit transaction on this
// DB handle (setting trxAutomatic) similar to how query() does in such situations.
$this
->
begin
(
__METHOD__
,
self
::
TRANSACTION_INTERNAL
);
}
$this
->
transactionManager
->
addPostCommitOrIdleCallback
(
$callback
,
$fname
);
if
(
!
$this
->
trxLevel
()
)
{
$dbErrors
=
[];
$this
->
runOnTransactionIdleCallbacks
(
self
::
TRIGGER_IDLE
,
$dbErrors
);
if
(
$dbErrors
)
{
throw
$dbErrors
[
0
];
}
}
}
final
public
function
onTransactionPreCommitOrIdle
(
callable
$callback
,
$fname
=
__METHOD__
)
{
if
(
!
$this
->
trxLevel
()
&&
$this
->
getTransactionRoundId
()
)
{
// This DB handle is set to participate in LoadBalancer transaction rounds and
// an explicit transaction round is active. Start an implicit transaction on this
// DB handle (setting trxAutomatic) similar to how query() does in such situations.
$this
->
begin
(
__METHOD__
,
self
::
TRANSACTION_INTERNAL
);
}
if
(
$this
->
trxLevel
()
)
{
$this
->
transactionManager
->
addPreCommitOrIdleCallback
(
$callback
,
$fname
);
}
else
{
// No transaction is active nor will start implicitly, so make one for this callback
$this
->
startAtomic
(
__METHOD__
,
self
::
ATOMIC_CANCELABLE
);
try
{
$callback
(
$this
);
}
catch
(
Throwable
$e
)
{
// Avoid confusing error reporting during critical section errors
if
(
!
$this
->
csmError
)
{
$this
->
cancelAtomic
(
__METHOD__
);
}
throw
$e
;
}
$this
->
endAtomic
(
__METHOD__
);
}
}
final
public
function
onAtomicSectionCancel
(
callable
$callback
,
$fname
=
__METHOD__
)
{
$this
->
transactionManager
->
onAtomicSectionCancel
(
$this
,
$callback
,
$fname
);
}
final
public
function
setTransactionListener
(
$name
,
?
callable
$callback
=
null
)
{
$this
->
transactionManager
->
setTransactionListener
(
$name
,
$callback
);
}
/**
* Whether to disable running of post-COMMIT/ROLLBACK callbacks
*
* @internal This method should not be used outside of Database/LoadBalancer
*
* @since 1.28
* @param bool $suppress
*/
final
public
function
setTrxEndCallbackSuppression
(
$suppress
)
{
$this
->
transactionManager
->
setTrxEndCallbackSuppression
(
$suppress
);
}
/**
* Consume and run any "on transaction idle/resolution" callbacks
*
* @internal This method should not be used outside of Database/LoadBalancer
*
* @since 1.20
* @param int $trigger IDatabase::TRIGGER_* constant
* @param DBError[] &$errors DB exceptions caught [returned]
* @return int Number of callbacks attempted
* @throws DBUnexpectedError
* @throws Throwable Any non-DBError exception thrown by a callback
*/
public
function
runOnTransactionIdleCallbacks
(
$trigger
,
array
&
$errors
=
[]
)
{
if
(
$this
->
trxLevel
()
)
{
throw
new
DBUnexpectedError
(
$this
,
__METHOD__
.
': a transaction is still open'
);
}
if
(
$this
->
transactionManager
->
isEndCallbacksSuppressed
()
)
{
// Execution deferred by LoadBalancer for explicit execution later
return
0
;
}
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
$count
=
0
;
$autoTrx
=
$this
->
flagsHolder
->
hasImplicitTrxFlag
();
// automatic begin() enabled?
// Drain the queues of transaction "idle" and "end" callbacks until they are empty
do
{
$callbackEntries
=
$this
->
transactionManager
->
consumeEndCallbacks
(
$trigger
);
$count
+=
count
(
$callbackEntries
);
foreach
(
$callbackEntries
as
$entry
)
{
$this
->
flagsHolder
->
clearFlag
(
self
::
DBO_TRX
);
// make each query its own transaction
try
{
$entry
[
0
](
$trigger
,
$this
);
}
catch
(
DBError
$ex
)
{
call_user_func
(
$this
->
errorLogger
,
$ex
);
$errors
[]
=
$ex
;
// Some callbacks may use startAtomic/endAtomic, so make sure
// their transactions are ended so other callbacks don't fail
if
(
$this
->
trxLevel
()
)
{
$this
->
rollback
(
__METHOD__
,
self
::
FLUSHING_INTERNAL
);
}
}
finally
{
if
(
$autoTrx
)
{
$this
->
flagsHolder
->
setFlag
(
self
::
DBO_TRX
);
// restore automatic begin()
}
else
{
$this
->
flagsHolder
->
clearFlag
(
self
::
DBO_TRX
);
// restore auto-commit
}
}
}
}
while
(
$this
->
transactionManager
->
countPostCommitOrIdleCallbacks
()
);
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
return
$count
;
}
/**
* Actually run any "transaction listener" callbacks
*
* @internal This method should not be used outside of Database/LoadBalancer
*
* @since 1.20
* @param int $trigger IDatabase::TRIGGER_* constant
* @param DBError[] &$errors DB exceptions caught [returned]
* @throws Throwable Any non-DBError exception thrown by a callback
*/
public
function
runTransactionListenerCallbacks
(
$trigger
,
array
&
$errors
=
[]
)
{
if
(
$this
->
transactionManager
->
isEndCallbacksSuppressed
()
)
{
// Execution deferred by LoadBalancer for explicit execution later
return
;
}
// These callbacks should only be registered in setup, thus no iteration is needed
foreach
(
$this
->
transactionManager
->
getRecurringCallbacks
()
as
$callback
)
{
try
{
$callback
(
$trigger
,
$this
);
}
catch
(
DBError
$ex
)
{
(
$this
->
errorLogger
)(
$ex
);
$errors
[]
=
$ex
;
}
}
}
/**
* Handle "on transaction idle/resolution" and "transaction listener" callbacks post-COMMIT
*
* @throws DBError The first DBError exception thrown by a callback
* @throws Throwable Any non-DBError exception thrown by a callback
*/
private
function
runTransactionPostCommitCallbacks
()
{
$dbErrors
=
[];
$this
->
runOnTransactionIdleCallbacks
(
self
::
TRIGGER_COMMIT
,
$dbErrors
);
$this
->
runTransactionListenerCallbacks
(
self
::
TRIGGER_COMMIT
,
$dbErrors
);
$this
->
lastEmulatedAffectedRows
=
0
;
// for the sake of consistency
if
(
$dbErrors
)
{
throw
$dbErrors
[
0
];
}
}
/**
* Handle "on transaction idle/resolution" and "transaction listener" callbacks post-ROLLBACK
*
* This will suppress and log any DBError exceptions
*
* @throws Throwable Any non-DBError exception thrown by a callback
*/
private
function
runTransactionPostRollbackCallbacks
()
{
$this
->
runOnTransactionIdleCallbacks
(
self
::
TRIGGER_ROLLBACK
);
$this
->
runTransactionListenerCallbacks
(
self
::
TRIGGER_ROLLBACK
);
$this
->
lastEmulatedAffectedRows
=
0
;
// for the sake of consistency
}
final
public
function
startAtomic
(
$fname
=
__METHOD__
,
$cancelable
=
self
::
ATOMIC_NOT_CANCELABLE
)
{
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
if
(
$this
->
trxLevel
()
)
{
// This atomic section is only one part of a larger transaction
$sectionOwnsTrx
=
false
;
}
else
{
// Start an implicit transaction (sets trxAutomatic)
try
{
$this
->
begin
(
$fname
,
self
::
TRANSACTION_INTERNAL
);
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
throw
$e
;
}
if
(
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
)
{
// This DB handle participates in LoadBalancer transaction rounds; all atomic
// sections should be buffered into one transaction (e.g. to keep web requests
// transactional). Note that an implicit transaction round is considered to be
// active when no there is no explicit transaction round.
$sectionOwnsTrx
=
false
;
}
else
{
// This DB handle does not participate in LoadBalancer transaction rounds;
// each topmost atomic section will use its own transaction.
$sectionOwnsTrx
=
true
;
}
$this
->
transactionManager
->
setAutomaticAtomic
(
$sectionOwnsTrx
);
}
if
(
$cancelable
===
self
::
ATOMIC_CANCELABLE
)
{
if
(
$sectionOwnsTrx
)
{
// This atomic section is synonymous with the whole transaction; just
// use full COMMIT/ROLLBACK in endAtomic()/cancelAtomic(), respectively
$savepointId
=
self
::
NOT_APPLICABLE
;
}
else
{
// This atomic section is only part of the whole transaction; use a SAVEPOINT
// query so that its changes can be cancelled without losing the rest of the
// transaction (e.g. changes from other sections or from outside of sections)
try
{
$savepointId
=
$this
->
transactionManager
->
nextSavePointId
(
$this
,
$fname
);
$sql
=
$this
->
platform
->
savepointSqlText
(
$savepointId
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_TRX
,
'SAVEPOINT'
);
$this
->
query
(
$query
,
$fname
);
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
,
$e
);
throw
$e
;
}
}
}
else
{
$savepointId
=
null
;
}
$sectionId
=
new
AtomicSectionIdentifier
;
$this
->
transactionManager
->
addToAtomicLevels
(
$fname
,
$sectionId
,
$savepointId
);
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
return
$sectionId
;
}
final
public
function
endAtomic
(
$fname
=
__METHOD__
)
{
[
$savepointId
,
$sectionId
]
=
$this
->
transactionManager
->
onEndAtomic
(
$this
,
$fname
);
$runPostCommitCallbacks
=
false
;
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
// Remove the last section (no need to re-index the array)
$finalLevelOfImplicitTrxPopped
=
$this
->
transactionManager
->
popAtomicLevel
();
try
{
if
(
$finalLevelOfImplicitTrxPopped
)
{
$this
->
commit
(
$fname
,
self
::
FLUSHING_INTERNAL
);
$runPostCommitCallbacks
=
true
;
}
elseif
(
$savepointId
!==
null
&&
$savepointId
!==
self
::
NOT_APPLICABLE
)
{
$sql
=
$this
->
platform
->
releaseSavepointSqlText
(
$savepointId
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_TRX
,
'RELEASE SAVEPOINT'
);
$this
->
query
(
$query
,
$fname
);
}
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
,
$e
);
throw
$e
;
}
$this
->
transactionManager
->
onEndAtomicInCriticalSection
(
$sectionId
);
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
if
(
$runPostCommitCallbacks
)
{
$this
->
runTransactionPostCommitCallbacks
();
}
}
final
public
function
cancelAtomic
(
$fname
=
__METHOD__
,
?
AtomicSectionIdentifier
$sectionId
=
null
)
{
$this
->
transactionManager
->
onCancelAtomicBeforeCriticalSection
(
$this
,
$fname
);
$pos
=
$this
->
transactionManager
->
getPositionFromSectionId
(
$sectionId
);
if
(
$pos
<
0
)
{
throw
new
DBUnexpectedError
(
$this
,
"Atomic section not found (for $fname)"
);
}
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
$runPostRollbackCallbacks
=
false
;
[
$savedFname
,
$excisedSectionIds
,
$newTopSectionId
,
$savedSectionId
,
$savepointId
]
=
$this
->
transactionManager
->
cancelAtomic
(
$pos
);
try
{
if
(
$savedFname
!==
$fname
)
{
$e
=
new
DBUnexpectedError
(
$this
,
"Invalid atomic section ended (got $fname but expected $savedFname)"
);
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
,
$e
);
throw
$e
;
}
// Remove the last section (no need to re-index the array)
$this
->
transactionManager
->
popAtomicLevel
();
$excisedSectionIds
[]
=
$savedSectionId
;
$newTopSectionId
=
$this
->
transactionManager
->
currentAtomicSectionId
();
if
(
$savepointId
!==
null
)
{
// Rollback the transaction changes proposed within this atomic section
if
(
$savepointId
===
self
::
NOT_APPLICABLE
)
{
// Atomic section started the transaction; rollback the whole transaction
// and trigger cancellation callbacks for all active atomic sections
$this
->
rollback
(
$fname
,
self
::
FLUSHING_INTERNAL
);
$runPostRollbackCallbacks
=
true
;
}
else
{
// Atomic section nested within the transaction; rollback the transaction
// to the state prior to this section and trigger its cancellation callbacks
$sql
=
$this
->
platform
->
rollbackToSavepointSqlText
(
$savepointId
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_TRX
,
'ROLLBACK TO SAVEPOINT'
);
$this
->
query
(
$query
,
$fname
);
$this
->
transactionManager
->
setTrxStatusToOk
();
// no exception; recovered
$this
->
transactionManager
->
runOnAtomicSectionCancelCallbacks
(
$this
,
self
::
TRIGGER_CANCEL
,
$excisedSectionIds
);
}
}
else
{
// Put the transaction into an error state if it's not already in one
$trxError
=
new
DBUnexpectedError
(
$this
,
"Uncancelable atomic section canceled (got $fname)"
);
$this
->
transactionManager
->
setTransactionError
(
$trxError
);
}
}
finally
{
// Fix up callbacks owned by the sections that were just cancelled.
// All callbacks should have an owner that is present in trxAtomicLevels.
$this
->
transactionManager
->
modifyCallbacksForCancel
(
$excisedSectionIds
,
$newTopSectionId
);
}
$this
->
lastEmulatedAffectedRows
=
0
;
// for the sake of consistency
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
if
(
$runPostRollbackCallbacks
)
{
$this
->
runTransactionPostRollbackCallbacks
();
}
}
final
public
function
doAtomicSection
(
$fname
,
callable
$callback
,
$cancelable
=
self
::
ATOMIC_NOT_CANCELABLE
)
{
$sectionId
=
$this
->
startAtomic
(
$fname
,
$cancelable
);
try
{
$res
=
$callback
(
$this
,
$fname
);
}
catch
(
Throwable
$e
)
{
// Avoid confusing error reporting during critical section errors
if
(
!
$this
->
csmError
)
{
$this
->
cancelAtomic
(
$fname
,
$sectionId
);
}
throw
$e
;
}
$this
->
endAtomic
(
$fname
);
return
$res
;
}
final
public
function
begin
(
$fname
=
__METHOD__
,
$mode
=
self
::
TRANSACTION_EXPLICIT
)
{
static
$modes
=
[
self
::
TRANSACTION_EXPLICIT
,
self
::
TRANSACTION_INTERNAL
];
if
(
!
in_array
(
$mode
,
$modes
,
true
)
)
{
throw
new
DBUnexpectedError
(
$this
,
"$fname: invalid mode parameter '$mode'"
);
}
$this
->
transactionManager
->
onBegin
(
$this
,
$fname
);
if
(
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
&&
$mode
!==
self
::
TRANSACTION_INTERNAL
)
{
$msg
=
"$fname: implicit transaction expected (DBO_TRX set)"
;
throw
new
DBUnexpectedError
(
$this
,
$msg
);
}
$this
->
assertHasConnectionHandle
();
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
$timeStart
=
microtime
(
true
);
try
{
$this
->
doBegin
(
$fname
);
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
throw
$e
;
}
$timeEnd
=
microtime
(
true
);
// Treat "BEGIN" as a trivial query to gauge the RTT delay
$rtt
=
max
(
$timeEnd
-
$timeStart
,
0.0
);
$this
->
transactionManager
->
onBeginInCriticalSection
(
$mode
,
$fname
,
$rtt
);
$this
->
replicationReporter
->
resetReplicationLagStatus
(
$this
);
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
}
/**
* Issues the BEGIN command to the database server.
*
* @see Database::begin()
* @param string $fname
* @throws DBError
*/
protected
function
doBegin
(
$fname
)
{
$query
=
new
Query
(
'BEGIN'
,
self
::
QUERY_CHANGE_TRX
,
'BEGIN'
);
$this
->
query
(
$query
,
$fname
);
}
final
public
function
commit
(
$fname
=
__METHOD__
,
$flush
=
self
::
FLUSHING_ONE
)
{
static
$modes
=
[
self
::
FLUSHING_ONE
,
self
::
FLUSHING_ALL_PEERS
,
self
::
FLUSHING_INTERNAL
];
if
(
!
in_array
(
$flush
,
$modes
,
true
)
)
{
throw
new
DBUnexpectedError
(
$this
,
"$fname: invalid flush parameter '$flush'"
);
}
if
(
!
$this
->
transactionManager
->
onCommit
(
$this
,
$fname
,
$flush
)
)
{
return
;
}
$this
->
assertHasConnectionHandle
();
$this
->
runOnTransactionPreCommitCallbacks
();
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
try
{
if
(
$this
->
trxLevel
()
)
{
$query
=
new
Query
(
'COMMIT'
,
self
::
QUERY_CHANGE_TRX
,
'COMMIT'
);
$this
->
query
(
$query
,
$fname
);
}
}
catch
(
DBError
$e
)
{
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
throw
$e
;
}
$lastWriteTime
=
$this
->
transactionManager
->
onCommitInCriticalSection
(
$this
);
if
(
$lastWriteTime
)
{
$this
->
lastWriteTime
=
$lastWriteTime
;
}
// With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
// within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
// the Database caller during a safe point. This avoids isolation and recursion issues.
if
(
$flush
===
self
::
FLUSHING_ONE
)
{
$this
->
runTransactionPostCommitCallbacks
();
}
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
}
final
public
function
rollback
(
$fname
=
__METHOD__
,
$flush
=
self
::
FLUSHING_ONE
)
{
if
(
$flush
!==
self
::
FLUSHING_INTERNAL
&&
$flush
!==
self
::
FLUSHING_ALL_PEERS
&&
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
)
{
throw
new
DBUnexpectedError
(
$this
,
"$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
);
}
if
(
!
$this
->
trxLevel
()
)
{
$this
->
transactionManager
->
setTrxStatusToNone
();
$this
->
transactionManager
->
clearPreEndCallbacks
();
if
(
$this
->
transactionManager
->
trxLevel
()
===
TransactionManager
::
STATUS_TRX_ERROR
)
{
$this
->
logger
->
info
(
"$fname: acknowledged server-side transaction loss on {db_server}"
,
$this
->
getLogContext
()
);
}
return
;
}
$this
->
assertHasConnectionHandle
();
if
(
$this
->
csmError
)
{
// Since the session state is corrupt, we cannot just rollback the transaction
// while preserving the non-transaction session state. The handle will remain
// marked as corrupt until flushSession() is called to reset the connection
// and deal with any remaining callbacks.
$this
->
logger
->
info
(
"$fname: acknowledged client-side transaction loss on {db_server}"
,
$this
->
getLogContext
()
);
return
;
}
$cs
=
$this
->
commenceCriticalSection
(
__METHOD__
);
if
(
$this
->
trxLevel
()
)
{
// Disconnects cause rollback anyway, so ignore those errors
$query
=
new
Query
(
$this
->
platform
->
rollbackSqlText
(),
self
::
QUERY_SILENCE_ERRORS
|
self
::
QUERY_CHANGE_TRX
,
'ROLLBACK'
);
$this
->
query
(
$query
,
$fname
);
}
$this
->
transactionManager
->
onRollbackInCriticalSection
(
$this
);
// With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
// within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
// the Database caller during a safe point. This avoids isolation and recursion issues.
if
(
$flush
===
self
::
FLUSHING_ONE
)
{
$this
->
runTransactionPostRollbackCallbacks
();
}
$this
->
completeCriticalSection
(
__METHOD__
,
$cs
);
}
/**
* @internal Only for tests and highly discouraged
* @param TransactionManager $transactionManager
*/
public
function
setTransactionManager
(
TransactionManager
$transactionManager
)
{
$this
->
transactionManager
=
$transactionManager
;
}
public
function
flushSession
(
$fname
=
__METHOD__
,
$flush
=
self
::
FLUSHING_ONE
)
{
if
(
$flush
!==
self
::
FLUSHING_INTERNAL
&&
$flush
!==
self
::
FLUSHING_ALL_PEERS
&&
$this
->
flagsHolder
->
hasImplicitTrxFlag
()
)
{
throw
new
DBUnexpectedError
(
$this
,
"$fname: Expected mass flush of all peer connections (DBO_TRX set)"
);
}
if
(
$this
->
csmError
)
{
// If a critical section error occurred, such as Excimer timeout exceptions raised
// before a query response was marshalled, destroy the connection handle and reset
// the session state tracking variables. The value of trxLevel() is irrelevant here,
// and, in fact, might be 1 due to rollback() deferring critical section recovery.
$this
->
logger
->
info
(
"$fname: acknowledged client-side session loss on {db_server}"
,
$this
->
getLogContext
()
);
$this
->
csmError
=
null
;
$this
->
csmFname
=
null
;
$this
->
replaceLostConnection
(
2048
,
__METHOD__
);
return
;
}
if
(
$this
->
trxLevel
()
)
{
// Any existing transaction should have been rolled back already
throw
new
DBUnexpectedError
(
$this
,
"$fname: transaction still in progress (not yet rolled back)"
);
}
if
(
$this
->
transactionManager
->
sessionStatus
()
===
TransactionManager
::
STATUS_SESS_ERROR
)
{
// If the session state was already lost due to either an unacknowledged session
// state loss error (e.g. dropped connection) or an explicit connection close call,
// then there is nothing to do here. Note that in such cases, even temporary tables
// and server-side config variables are lost (invocation of this method is assumed
// to imply that such losses are tolerable).
$this
->
logger
->
info
(
"$fname: acknowledged server-side session loss on {db_server}"
,
$this
->
getLogContext
()
);
}
elseif
(
$this
->
isOpen
()
)
{
// Connection handle exists; server-side session state must be flushed
$this
->
doFlushSession
(
$fname
);
$this
->
sessionNamedLocks
=
[];
}
$this
->
transactionManager
->
clearSessionError
();
}
/**
* Reset the server-side session state for named locks and table locks
*
* Connection and query errors will be suppressed and logged
*
* @param string $fname
* @since 1.38
*/
protected
function
doFlushSession
(
$fname
)
{
// no-op
}
public
function
flushSnapshot
(
$fname
=
__METHOD__
,
$flush
=
self
::
FLUSHING_ONE
)
{
$this
->
transactionManager
->
onFlushSnapshot
(
$this
,
$fname
,
$flush
,
$this
->
getTransactionRoundId
()
);
if
(
$this
->
transactionManager
->
sessionStatus
()
===
TransactionManager
::
STATUS_SESS_ERROR
||
$this
->
transactionManager
->
trxStatus
()
===
TransactionManager
::
STATUS_TRX_ERROR
)
{
$this
->
rollback
(
$fname
,
self
::
FLUSHING_INTERNAL
);
}
else
{
$this
->
commit
(
$fname
,
self
::
FLUSHING_INTERNAL
);
}
}
public
function
duplicateTableStructure
(
$oldName
,
$newName
,
$temporary
=
false
,
$fname
=
__METHOD__
)
{
throw
new
RuntimeException
(
__METHOD__
.
' is not implemented in descendant class'
);
}
public
function
listTables
(
$prefix
=
null
,
$fname
=
__METHOD__
)
{
throw
new
RuntimeException
(
__METHOD__
.
' is not implemented in descendant class'
);
}
public
function
affectedRows
()
{
$this
->
lastEmulatedAffectedRows
??=
$this
->
lastQueryAffectedRows
;
return
$this
->
lastEmulatedAffectedRows
;
}
public
function
insertId
()
{
if
(
$this
->
lastEmulatedInsertId
===
null
)
{
// Guard against misuse of this method by checking affectedRows(). Note that calls
// to insert() with "IGNORE" and calls to insertSelect() might not add any rows.
if
(
$this
->
affectedRows
()
)
{
$this
->
lastEmulatedInsertId
=
$this
->
lastInsertId
();
}
else
{
$this
->
lastEmulatedInsertId
=
0
;
}
}
return
$this
->
lastEmulatedInsertId
;
}
/**
* Get a row ID from the last insert statement to implicitly assign one within the session
*
* If the statement involved assigning sequence IDs to multiple rows, then the return value
* will be any one of those values (database-specific). If the statement was an "UPSERT" and
* some existing rows were updated, then the result will either reflect only IDs of created
* rows or it will reflect IDs of both created and updated rows (this is database-specific).
*
* The result is unspecified if the statement gave an error.
*
* @return int Sequence ID, 0 (if none)
* @throws DBError
*/
abstract
protected
function
lastInsertId
();
public
function
ping
()
{
if
(
$this
->
isOpen
()
)
{
// If the connection was recently used, assume that it is still good
if
(
(
microtime
(
true
)
-
$this
->
lastPing
)
<
self
::
PING_TTL
)
{
return
true
;
}
// Send a trivial query to test the connection, triggering an automatic
// reconnection attempt if the connection was lost
$query
=
new
Query
(
self
::
PING_QUERY
,
self
::
QUERY_IGNORE_DBO_TRX
|
self
::
QUERY_SILENCE_ERRORS
|
self
::
QUERY_CHANGE_NONE
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
__METHOD__
);
$ok
=
(
$res
!==
false
);
}
else
{
// Try to re-establish a connection
$ok
=
$this
->
replaceLostConnection
(
null
,
__METHOD__
);
}
return
$ok
;
}
/**
* Close any existing (dead) database connection and open a new connection
*
* @param int|null $lastErrno
* @param string $fname
* @return bool True if new connection is opened successfully, false if error
*/
protected
function
replaceLostConnection
(
$lastErrno
,
$fname
)
{
if
(
$this
->
conn
)
{
$this
->
closeConnection
();
$this
->
conn
=
null
;
$this
->
handleSessionLossPreconnect
();
}
try
{
$this
->
open
(
$this
->
connectionParams
[
self
::
CONN_HOST
],
$this
->
connectionParams
[
self
::
CONN_USER
],
$this
->
connectionParams
[
self
::
CONN_PASSWORD
],
$this
->
currentDomain
->
getDatabase
(),
$this
->
currentDomain
->
getSchema
(),
$this
->
tablePrefix
()
);
$this
->
lastPing
=
microtime
(
true
);
$ok
=
true
;
$this
->
logger
->
warning
(
$fname
.
': lost connection to {db_server} with error {errno}; reconnected'
,
$this
->
getLogContext
(
[
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'connection'
,
'errno'
=>
$lastErrno
]
)
);
}
catch
(
DBConnectionError
$e
)
{
$ok
=
false
;
$this
->
logger
->
error
(
$fname
.
': lost connection to {db_server} with error {errno}; reconnection failed: {connect_msg}'
,
$this
->
getLogContext
(
[
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'connection'
,
'errno'
=>
$lastErrno
,
'connect_msg'
=>
$e
->
getMessage
()
]
)
);
}
// Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
// If callback suppression is set then the array will remain unhandled.
$this
->
runOnTransactionIdleCallbacks
(
self
::
TRIGGER_ROLLBACK
);
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener().
// If callback suppression is set then the array will remain unhandled.
$this
->
runTransactionListenerCallbacks
(
self
::
TRIGGER_ROLLBACK
);
return
$ok
;
}
/**
* Merge the result of getSessionLagStatus() for several DBs
* using the most pessimistic values to estimate the lag of
* any data derived from them in combination
*
* This is information is useful for caching modules
*
* @see WANObjectCache::set()
* @see WANObjectCache::getWithSetCallback()
*
* @param IReadableDatabase|null ...$dbs
* Note: For backward compatibility, it is allowed for null values
* to be passed among the parameters. This is deprecated since 1.36,
* only IReadableDatabase objects should be passed.
*
* @return array Map of values:
* - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
* - since: oldest UNIX timestamp of any of the DB lag estimates
* - pending: whether any of the DBs have uncommitted changes
* @throws DBError
* @since 1.27
*/
public
static
function
getCacheSetOptions
(
?
IReadableDatabase
...
$dbs
)
{
$res
=
[
'lag'
=>
0
,
'since'
=>
INF
,
'pending'
=>
false
];
foreach
(
func_get_args
()
as
$db
)
{
if
(
$db
instanceof
IReadableDatabase
)
{
$status
=
$db
->
getSessionLagStatus
();
if
(
$status
[
'lag'
]
===
false
)
{
$res
[
'lag'
]
=
false
;
}
elseif
(
$res
[
'lag'
]
!==
false
)
{
$res
[
'lag'
]
=
max
(
$res
[
'lag'
],
$status
[
'lag'
]
);
}
$res
[
'since'
]
=
min
(
$res
[
'since'
],
$status
[
'since'
]
);
}
if
(
$db
instanceof
IDatabaseForOwner
)
{
$res
[
'pending'
]
=
$res
[
'pending'
]
?:
$db
->
writesPending
();
}
}
return
$res
;
}
public
function
encodeBlob
(
$b
)
{
return
$b
;
}
public
function
decodeBlob
(
$b
)
{
if
(
$b
instanceof
Blob
)
{
$b
=
$b
->
fetch
();
}
return
$b
;
}
public
function
setSessionOptions
(
array
$options
)
{
}
public
function
sourceFile
(
$filename
,
?
callable
$lineCallback
=
null
,
?
callable
$resultCallback
=
null
,
$fname
=
false
,
?
callable
$inputCallback
=
null
)
{
AtEase
::
suppressWarnings
();
$fp
=
fopen
(
$filename
,
'r'
);
AtEase
::
restoreWarnings
();
if
(
$fp
===
false
)
{
throw
new
RuntimeException
(
"Could not open
\"
{$filename}
\"
"
);
}
if
(
!
$fname
)
{
$fname
=
__METHOD__
.
"( $filename )"
;
}
try
{
return
$this
->
sourceStream
(
$fp
,
$lineCallback
,
$resultCallback
,
$fname
,
$inputCallback
);
}
finally
{
fclose
(
$fp
);
}
}
public
function
sourceStream
(
$fp
,
?
callable
$lineCallback
=
null
,
?
callable
$resultCallback
=
null
,
$fname
=
__METHOD__
,
?
callable
$inputCallback
=
null
)
{
$delimiterReset
=
new
ScopedCallback
(
function
(
$delimiter
)
{
$this
->
delimiter
=
$delimiter
;
},
[
$this
->
delimiter
]
);
$cmd
=
''
;
while
(
!
feof
(
$fp
)
)
{
if
(
$lineCallback
)
{
call_user_func
(
$lineCallback
);
}
$line
=
trim
(
fgets
(
$fp
)
);
if
(
$line
==
''
)
{
continue
;
}
if
(
$line
[
0
]
==
'-'
&&
$line
[
1
]
==
'-'
)
{
continue
;
}
if
(
$cmd
!=
''
)
{
$cmd
.=
' '
;
}
$done
=
$this
->
streamStatementEnd
(
$cmd
,
$line
);
$cmd
.=
"$line
\n
"
;
if
(
$done
||
feof
(
$fp
)
)
{
$cmd
=
$this
->
platform
->
replaceVars
(
$cmd
);
if
(
$inputCallback
)
{
$callbackResult
=
$inputCallback
(
$cmd
);
if
(
is_string
(
$callbackResult
)
||
!
$callbackResult
)
{
$cmd
=
$callbackResult
;
}
}
if
(
$cmd
)
{
$res
=
$this
->
query
(
$cmd
,
$fname
);
if
(
$resultCallback
)
{
$resultCallback
(
$res
,
$this
);
}
if
(
$res
===
false
)
{
$err
=
$this
->
lastError
();
return
"Query
\"
{$cmd}
\"
failed with error code
\"
$err
\"
.
\n
"
;
}
}
$cmd
=
''
;
}
}
ScopedCallback
::
consume
(
$delimiterReset
);
return
true
;
}
/**
* Called by sourceStream() to check if we've reached a statement end
*
* @param string &$sql SQL assembled so far
* @param string &$newLine New line about to be added to $sql
* @return bool Whether $newLine contains end of the statement
*/
public
function
streamStatementEnd
(
&
$sql
,
&
$newLine
)
{
if
(
$this
->
delimiter
)
{
$prev
=
$newLine
;
$newLine
=
preg_replace
(
'/'
.
preg_quote
(
$this
->
delimiter
,
'/'
)
.
'$/'
,
''
,
$newLine
);
if
(
$newLine
!=
$prev
)
{
return
true
;
}
}
return
false
;
}
/**
* @inheritDoc
*/
public
function
lockIsFree
(
$lockName
,
$method
)
{
// RDBMs methods for checking named locks may or may not count this thread itself.
// In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
// the behavior chosen by the interface for this method.
if
(
isset
(
$this
->
sessionNamedLocks
[
$lockName
]
)
)
{
$lockIsFree
=
false
;
}
else
{
$lockIsFree
=
$this
->
doLockIsFree
(
$lockName
,
$method
);
}
return
$lockIsFree
;
}
/**
* @see lockIsFree()
*
* @param string $lockName
* @param string $method
* @return bool Success
* @throws DBError
*/
protected
function
doLockIsFree
(
string
$lockName
,
string
$method
)
{
return
true
;
// not implemented
}
/**
* @inheritDoc
*/
public
function
lock
(
$lockName
,
$method
,
$timeout
=
5
,
$flags
=
0
)
{
$lockTsUnix
=
$this
->
doLock
(
$lockName
,
$method
,
$timeout
);
if
(
$lockTsUnix
!==
null
)
{
$locked
=
true
;
$this
->
sessionNamedLocks
[
$lockName
]
=
[
'ts'
=>
$lockTsUnix
,
'trxId'
=>
$this
->
transactionManager
->
getTrxId
()
];
}
else
{
$locked
=
false
;
$this
->
logger
->
info
(
__METHOD__
.
": failed to acquire lock '{lockname}'"
,
[
'lockname'
=>
$lockName
,
'db_log_category'
=>
'locking'
]
);
}
return
$this
->
flagsHolder
::
contains
(
$flags
,
self
::
LOCK_TIMESTAMP
)
?
$lockTsUnix
:
$locked
;
}
/**
* @see lock()
*
* @param string $lockName
* @param string $method
* @param int $timeout
* @return float|null UNIX timestamp of lock acquisition; null on failure
* @throws DBError
*/
protected
function
doLock
(
string
$lockName
,
string
$method
,
int
$timeout
)
{
return
microtime
(
true
);
// not implemented
}
/**
* @inheritDoc
*/
public
function
unlock
(
$lockName
,
$method
)
{
if
(
!
isset
(
$this
->
sessionNamedLocks
[
$lockName
]
)
)
{
$released
=
false
;
$this
->
logger
->
warning
(
__METHOD__
.
": trying to release unheld lock '$lockName'
\n
"
,
[
'db_log_category'
=>
'locking'
]
);
}
else
{
$released
=
$this
->
doUnlock
(
$lockName
,
$method
);
if
(
$released
)
{
unset
(
$this
->
sessionNamedLocks
[
$lockName
]
);
}
else
{
$this
->
logger
->
warning
(
__METHOD__
.
": failed to release lock '$lockName'
\n
"
,
[
'db_log_category'
=>
'locking'
]
);
}
}
return
$released
;
}
/**
* @see unlock()
*
* @param string $lockName
* @param string $method
* @return bool Success
* @throws DBError
*/
protected
function
doUnlock
(
string
$lockName
,
string
$method
)
{
return
true
;
// not implemented
}
public
function
getScopedLockAndFlush
(
$lockKey
,
$fname
,
$timeout
)
{
$this
->
transactionManager
->
onGetScopedLockAndFlush
(
$this
,
$fname
);
if
(
!
$this
->
lock
(
$lockKey
,
$fname
,
$timeout
)
)
{
return
null
;
}
$unlocker
=
new
ScopedCallback
(
function
()
use
(
$lockKey
,
$fname
)
{
// Note that the callback can be reached due to an exception making the calling
// function end early. If the transaction/session is in an error state, avoid log
// spam and confusing replacement of an original DBError with one about unlock().
// Unlock query will fail anyway; avoid possibly triggering errors in rollback()
if
(
$this
->
transactionManager
->
sessionStatus
()
===
TransactionManager
::
STATUS_SESS_ERROR
||
$this
->
transactionManager
->
trxStatus
()
===
TransactionManager
::
STATUS_TRX_ERROR
)
{
return
;
}
if
(
$this
->
trxLevel
()
)
{
$this
->
onTransactionResolution
(
function
()
use
(
$lockKey
,
$fname
)
{
$this
->
unlock
(
$lockKey
,
$fname
);
},
$fname
);
}
else
{
$this
->
unlock
(
$lockKey
,
$fname
);
}
}
);
$this
->
commit
(
$fname
,
self
::
FLUSHING_INTERNAL
);
return
$unlocker
;
}
public
function
dropTable
(
$table
,
$fname
=
__METHOD__
)
{
if
(
!
$this
->
tableExists
(
$table
,
$fname
)
)
{
return
false
;
}
$query
=
new
Query
(
$this
->
platform
->
dropTableSqlText
(
$table
),
self
::
QUERY_CHANGE_SCHEMA
,
'DROP'
,
$table
);
$this
->
query
(
$query
,
$fname
);
return
true
;
}
public
function
truncateTable
(
$table
,
$fname
=
__METHOD__
)
{
$sql
=
"TRUNCATE TABLE "
.
$this
->
tableName
(
$table
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_SCHEMA
,
'TRUNCATE'
,
$table
);
$this
->
query
(
$query
,
$fname
);
}
public
function
isReadOnly
()
{
return
(
$this
->
getReadOnlyReason
()
!==
null
);
}
/**
* @return array|null Tuple of (reason string, "role" or "lb") if read-only; null otherwise
*/
protected
function
getReadOnlyReason
()
{
$reason
=
$this
->
replicationReporter
->
getTopologyBasedReadOnlyReason
();
if
(
$reason
)
{
return
$reason
;
}
$reason
=
$this
->
getLBInfo
(
self
::
LB_READ_ONLY_REASON
);
if
(
is_string
(
$reason
)
)
{
return
[
$reason
,
'lb'
];
}
return
null
;
}
/**
* Get the underlying binding connection handle
*
* Makes sure the connection resource is set (disconnects and ping() failure can unset it).
* This catches broken callers than catch and ignore disconnection exceptions.
* Unlike checking isOpen(), this is safe to call inside of open().
*
* @return mixed
* @throws DBUnexpectedError
* @since 1.26
*/
protected
function
getBindingHandle
()
{
if
(
!
$this
->
conn
)
{
throw
new
DBUnexpectedError
(
$this
,
'DB connection was already closed or the connection dropped'
);
}
return
$this
->
conn
;
}
/**
* Demark the start of a critical section of session/transaction state changes
*
* Use this to disable potentially DB handles due to corruption from highly unexpected
* exceptions (e.g. from zend timers or coding errors) preempting execution of methods.
*
* Callers must demark completion of the critical section with completeCriticalSection().
* Callers should handle DBError exceptions that do not cause object state corruption by
* catching them, calling completeCriticalSection(), and then rethrowing them.
*
* @code
* $cs = $this->commenceCriticalSection( __METHOD__ );
* try {
* //...send a query that changes the session/transaction state...
* } catch ( DBError $e ) {
* $this->completeCriticalSection( __METHOD__, $cs );
* throw $expectedException;
* }
* try {
* //...send another query that changes the session/transaction state...
* } catch ( DBError $trxError ) {
* // Require ROLLBACK before allowing any other queries from outside callers
* $this->completeCriticalSection( __METHOD__, $cs, $trxError );
* throw $expectedException;
* }
* // ...update session state fields of $this...
* $this->completeCriticalSection( __METHOD__, $cs );
* @endcode
*
* @see Database::completeCriticalSection()
*
* @since 1.36
* @param string $fname Caller name
* @return CriticalSectionScope|null RAII-style monitor (topmost sections only)
* @throws DBUnexpectedError If an unresolved critical section error already exists
*/
protected
function
commenceCriticalSection
(
string
$fname
)
{
if
(
$this
->
csmError
)
{
throw
new
DBUnexpectedError
(
$this
,
"Cannot execute $fname critical section while session state is out of sync.
\n\n
"
.
$this
->
csmError
->
getMessage
()
.
"
\n
"
.
$this
->
csmError
->
getTraceAsString
()
);
}
if
(
$this
->
csmId
)
{
$csm
=
null
;
// fold into the outer critical section
}
elseif
(
$this
->
csProvider
)
{
$csm
=
$this
->
csProvider
->
scopedEnter
(
$fname
,
null
,
// emergency limit (default)
null
,
// emergency callback (default)
function
()
use
(
$fname
)
{
// Mark a critical section as having been aborted by an error
$e
=
new
RuntimeException
(
"A critical section from {$fname} has failed"
);
$this
->
csmError
=
$e
;
$this
->
csmId
=
null
;
}
);
$this
->
csmId
=
$csm
->
getId
();
$this
->
csmFname
=
$fname
;
}
else
{
$csm
=
null
;
// not supported
}
return
$csm
;
}
/**
* Demark the completion of a critical section of session/transaction state changes
*
* @see Database::commenceCriticalSection()
*
* @since 1.36
* @param string $fname Caller name
* @param CriticalSectionScope|null $csm RAII-style monitor (topmost sections only)
* @param Throwable|null $trxError Error that requires setting STATUS_TRX_ERROR (if any)
*/
protected
function
completeCriticalSection
(
string
$fname
,
?
CriticalSectionScope
$csm
,
?
Throwable
$trxError
=
null
)
{
if
(
$csm
!==
null
)
{
if
(
$this
->
csmId
===
null
)
{
throw
new
LogicException
(
"$fname critical section is not active"
);
}
elseif
(
$csm
->
getId
()
!==
$this
->
csmId
)
{
throw
new
LogicException
(
"$fname critical section is not the active ({$this->csmFname}) one"
);
}
$csm
->
exit
();
$this
->
csmId
=
null
;
}
if
(
$trxError
)
{
$this
->
transactionManager
->
setTransactionError
(
$trxError
);
}
}
public
function
__toString
()
{
$id
=
spl_object_id
(
$this
);
$description
=
$this
->
getType
()
.
' object #'
.
$id
;
// phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
if
(
is_resource
(
$this
->
conn
)
)
{
$description
.=
' ('
.
(
string
)
$this
->
conn
.
')'
;
// "resource id #<ID>"
}
elseif
(
is_object
(
$this
->
conn
)
)
{
$handleId
=
spl_object_id
(
$this
->
conn
);
$description
.=
" (handle id #$handleId)"
;
}
return
$description
;
}
/**
* Make sure that copies do not share the same client binding handle
* @throws DBConnectionError
*/
public
function
__clone
()
{
$this
->
logger
->
warning
(
"Cloning "
.
static
::
class
.
" is not recommended; forking connection"
,
[
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'connection'
]
);
if
(
$this
->
isOpen
()
)
{
// Open a new connection resource without messing with the old one
$this
->
conn
=
null
;
$this
->
transactionManager
->
clearEndCallbacks
();
$this
->
handleSessionLossPreconnect
();
// no trx or locks anymore
$this
->
open
(
$this
->
connectionParams
[
self
::
CONN_HOST
],
$this
->
connectionParams
[
self
::
CONN_USER
],
$this
->
connectionParams
[
self
::
CONN_PASSWORD
],
$this
->
currentDomain
->
getDatabase
(),
$this
->
currentDomain
->
getSchema
(),
$this
->
tablePrefix
()
);
$this
->
lastPing
=
microtime
(
true
);
}
}
/**
* Called by serialize. Throw an exception when DB connection is serialized.
* This causes problems on some database engines because the connection is
* not restored on unserialize.
* @return never
*/
public
function
__sleep
()
{
throw
new
RuntimeException
(
'Database serialization may cause problems, since '
.
'the connection is not restored on wakeup'
);
}
/**
* Run a few simple checks and close dangling connections
*/
public
function
__destruct
()
{
if
(
$this
->
transactionManager
)
{
// Tests mock this class and disable constructor.
$this
->
transactionManager
->
onDestruct
();
}
$danglingWriters
=
$this
->
pendingWriteAndCallbackCallers
();
if
(
$danglingWriters
)
{
$fnames
=
implode
(
', '
,
$danglingWriters
);
trigger_error
(
"DB transaction writes or callbacks still pending ($fnames)"
);
}
if
(
$this
->
conn
)
{
// Avoid connection leaks. Normally, resources close at script completion.
// The connection might already be closed in PHP by now, so suppress warnings.
AtEase
::
suppressWarnings
();
$this
->
closeConnection
();
AtEase
::
restoreWarnings
();
$this
->
conn
=
null
;
}
}
/* Start of methods delegated to DatabaseFlags. Avoid using them outside of rdbms library */
public
function
setFlag
(
$flag
,
$remember
=
self
::
REMEMBER_NOTHING
)
{
$this
->
flagsHolder
->
setFlag
(
$flag
,
$remember
);
}
public
function
clearFlag
(
$flag
,
$remember
=
self
::
REMEMBER_NOTHING
)
{
$this
->
flagsHolder
->
clearFlag
(
$flag
,
$remember
);
}
public
function
restoreFlags
(
$state
=
self
::
RESTORE_PRIOR
)
{
$this
->
flagsHolder
->
restoreFlags
(
$state
);
}
public
function
getFlag
(
$flag
)
{
return
$this
->
flagsHolder
->
getFlag
(
$flag
);
}
/* End of methods delegated to DatabaseFlags. */
/* Start of methods delegated to TransactionManager. Avoid using them outside of rdbms library */
final
public
function
trxLevel
()
{
// FIXME: A lot of tests disable constructor leading to trx manager being
// null and breaking, this is unacceptable but hopefully this should
// happen less by moving these functions to the transaction manager class.
if
(
!
$this
->
transactionManager
)
{
$this
->
transactionManager
=
new
TransactionManager
(
new
NullLogger
()
);
}
return
$this
->
transactionManager
->
trxLevel
();
}
public
function
trxTimestamp
()
{
return
$this
->
transactionManager
->
trxTimestamp
();
}
public
function
trxStatus
()
{
return
$this
->
transactionManager
->
trxStatus
();
}
public
function
writesPending
()
{
return
$this
->
transactionManager
->
writesPending
();
}
public
function
writesOrCallbacksPending
()
{
return
$this
->
transactionManager
->
writesOrCallbacksPending
();
}
public
function
pendingWriteQueryDuration
(
$type
=
self
::
ESTIMATE_TOTAL
)
{
return
$this
->
transactionManager
->
pendingWriteQueryDuration
(
$type
);
}
public
function
pendingWriteCallers
()
{
if
(
!
$this
->
transactionManager
)
{
return
[];
}
return
$this
->
transactionManager
->
pendingWriteCallers
();
}
public
function
pendingWriteAndCallbackCallers
()
{
if
(
!
$this
->
transactionManager
)
{
return
[];
}
return
$this
->
transactionManager
->
pendingWriteAndCallbackCallers
();
}
public
function
runOnTransactionPreCommitCallbacks
()
{
return
$this
->
transactionManager
->
runOnTransactionPreCommitCallbacks
(
$this
);
}
public
function
explicitTrxActive
()
{
return
$this
->
transactionManager
->
explicitTrxActive
();
}
/* End of methods delegated to TransactionManager. */
/* Start of methods delegated to SQLPlatform. Avoid using them outside of rdbms library */
public
function
implicitOrderby
()
{
return
$this
->
platform
->
implicitOrderby
();
}
public
function
selectSQLText
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
return
$this
->
platform
->
selectSQLText
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
);
}
public
function
buildComparison
(
string
$op
,
array
$conds
):
string
{
return
$this
->
platform
->
buildComparison
(
$op
,
$conds
);
}
public
function
makeList
(
array
$a
,
$mode
=
self
::
LIST_COMMA
)
{
return
$this
->
platform
->
makeList
(
$a
,
$mode
);
}
public
function
makeWhereFrom2d
(
$data
,
$baseKey
,
$subKey
)
{
return
$this
->
platform
->
makeWhereFrom2d
(
$data
,
$baseKey
,
$subKey
);
}
public
function
factorConds
(
$condsArray
)
{
return
$this
->
platform
->
factorConds
(
$condsArray
);
}
public
function
bitNot
(
$field
)
{
return
$this
->
platform
->
bitNot
(
$field
);
}
public
function
bitAnd
(
$fieldLeft
,
$fieldRight
)
{
return
$this
->
platform
->
bitAnd
(
$fieldLeft
,
$fieldRight
);
}
public
function
bitOr
(
$fieldLeft
,
$fieldRight
)
{
return
$this
->
platform
->
bitOr
(
$fieldLeft
,
$fieldRight
);
}
public
function
buildConcat
(
$stringList
)
{
return
$this
->
platform
->
buildConcat
(
$stringList
);
}
public
function
buildGreatest
(
$fields
,
$values
)
{
return
$this
->
platform
->
buildGreatest
(
$fields
,
$values
);
}
public
function
buildLeast
(
$fields
,
$values
)
{
return
$this
->
platform
->
buildLeast
(
$fields
,
$values
);
}
public
function
buildSubstring
(
$input
,
$startPosition
,
$length
=
null
)
{
return
$this
->
platform
->
buildSubstring
(
$input
,
$startPosition
,
$length
);
}
public
function
buildStringCast
(
$field
)
{
return
$this
->
platform
->
buildStringCast
(
$field
);
}
public
function
buildIntegerCast
(
$field
)
{
return
$this
->
platform
->
buildIntegerCast
(
$field
);
}
public
function
tableName
(
string
$name
,
$format
=
'quoted'
)
{
return
$this
->
platform
->
tableName
(
$name
,
$format
);
}
public
function
tableNames
(
...
$tables
)
{
return
$this
->
platform
->
tableNames
(
...
$tables
);
}
public
function
tableNamesN
(
...
$tables
)
{
return
$this
->
platform
->
tableNamesN
(
...
$tables
);
}
public
function
addIdentifierQuotes
(
$s
)
{
return
$this
->
platform
->
addIdentifierQuotes
(
$s
);
}
public
function
isQuotedIdentifier
(
$name
)
{
return
$this
->
platform
->
isQuotedIdentifier
(
$name
);
}
public
function
buildLike
(
$param
,
...
$params
)
{
return
$this
->
platform
->
buildLike
(
$param
,
...
$params
);
}
public
function
anyChar
()
{
return
$this
->
platform
->
anyChar
();
}
public
function
anyString
()
{
return
$this
->
platform
->
anyString
();
}
public
function
limitResult
(
$sql
,
$limit
,
$offset
=
false
)
{
return
$this
->
platform
->
limitResult
(
$sql
,
$limit
,
$offset
);
}
public
function
unionSupportsOrderAndLimit
()
{
return
$this
->
platform
->
unionSupportsOrderAndLimit
();
}
public
function
unionQueries
(
$sqls
,
$all
,
$options
=
[]
)
{
return
$this
->
platform
->
unionQueries
(
$sqls
,
$all
,
$options
);
}
public
function
conditional
(
$cond
,
$caseTrueExpression
,
$caseFalseExpression
)
{
return
$this
->
platform
->
conditional
(
$cond
,
$caseTrueExpression
,
$caseFalseExpression
);
}
public
function
strreplace
(
$orig
,
$old
,
$new
)
{
return
$this
->
platform
->
strreplace
(
$orig
,
$old
,
$new
);
}
public
function
timestamp
(
$ts
=
0
)
{
return
$this
->
platform
->
timestamp
(
$ts
);
}
public
function
timestampOrNull
(
$ts
=
null
)
{
return
$this
->
platform
->
timestampOrNull
(
$ts
);
}
public
function
getInfinity
()
{
return
$this
->
platform
->
getInfinity
();
}
public
function
encodeExpiry
(
$expiry
)
{
return
$this
->
platform
->
encodeExpiry
(
$expiry
);
}
public
function
decodeExpiry
(
$expiry
,
$format
=
TS_MW
)
{
return
$this
->
platform
->
decodeExpiry
(
$expiry
,
$format
);
}
public
function
setTableAliases
(
array
$aliases
)
{
$this
->
platform
->
setTableAliases
(
$aliases
);
}
public
function
getTableAliases
()
{
return
$this
->
platform
->
getTableAliases
();
}
public
function
setIndexAliases
(
array
$aliases
)
{
$this
->
platform
->
setIndexAliases
(
$aliases
);
}
public
function
buildGroupConcatField
(
$delim
,
$tables
,
$field
,
$conds
=
''
,
$join_conds
=
[]
)
{
return
$this
->
platform
->
buildGroupConcatField
(
$delim
,
$tables
,
$field
,
$conds
,
$join_conds
);
}
public
function
buildSelectSubquery
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
return
$this
->
platform
->
buildSelectSubquery
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
);
}
public
function
buildExcludedValue
(
$column
)
{
return
$this
->
platform
->
buildExcludedValue
(
$column
);
}
public
function
setSchemaVars
(
$vars
)
{
$this
->
platform
->
setSchemaVars
(
$vars
);
}
/* End of methods delegated to SQLPlatform. */
/* Start of methods delegated to ReplicationReporter. */
public
function
primaryPosWait
(
DBPrimaryPos
$pos
,
$timeout
)
{
return
$this
->
replicationReporter
->
primaryPosWait
(
$this
,
$pos
,
$timeout
);
}
public
function
getPrimaryPos
()
{
return
$this
->
replicationReporter
->
getPrimaryPos
(
$this
);
}
public
function
getLag
()
{
return
$this
->
replicationReporter
->
getLag
(
$this
);
}
public
function
getSessionLagStatus
()
{
return
$this
->
replicationReporter
->
getSessionLagStatus
(
$this
);
}
/* End of methods delegated to ReplicationReporter. */
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 17 2026, 15:03 (10 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d7/0e/98f911be4697dc65cc6bbe890490
Default Alt Text
Database.php (108 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment