Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5959328
DatabaseMySQL.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
25 KB
Referenced Files
None
Subscribers
None
DatabaseMySQL.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
mysqli
;
use
mysqli_result
;
use
RuntimeException
;
use
Wikimedia\AtEase\AtEase
;
use
Wikimedia\IPUtils
;
use
Wikimedia\Rdbms\Platform\MySQLPlatform
;
use
Wikimedia\Rdbms\Platform\SQLPlatform
;
use
Wikimedia\Rdbms\Replication\MysqlReplicationReporter
;
/**
* MySQL database abstraction layer.
*
* Defines methods independent of the used MySQL extension.
*
* @property mysqli|null $conn
*
* @ingroup Database
* @since 1.22
* @see Database
*/
class
DatabaseMySQL
extends
Database
{
/** @var string|null */
private
$sslKeyPath
;
/** @var string|null */
private
$sslCertPath
;
/** @var string|null */
private
$sslCAFile
;
/** @var string|null */
private
$sslCAPath
;
/**
* Open SSL cipher list string
* @see https://docs.openssl.org/3.3/man1/openssl-ciphers/
* @var string|null
*/
private
$sslCiphers
;
/** @var bool Use experimental UTF-8 transmission encoding */
private
$utf8Mode
;
/** @var SQLPlatform */
protected
$platform
;
/** @var MysqlReplicationReporter */
protected
$replicationReporter
;
/** @var int Last implicit row ID for the session (0 if none) */
private
$sessionLastAutoRowId
;
/**
* Additional $params include:
* - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
* pt-heartbeat assumes the table is at heartbeat.heartbeat
* and uses UTC timestamps in the heartbeat.ts column.
* (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
* - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map.
* The "conds" key overrides the WHERE clause used to find the relevant row in the
* `heartbeat` table, e.g. ['shard' => 's1']. By default, the row used is the newest
* row having a server_id matching that of the immediate replication source server
* for the given replica.
* - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
* - sslKeyPath : path to key file [default: null]
* - sslCertPath : path to certificate file [default: null]
* - sslCAFile: path to a single certificate authority PEM file [default: null]
* - sslCAPath : parth to certificate authority PEM directory [default: null]
* - sslCiphers : array list of allowable ciphers [default: null]
* @param array $params
*/
public
function
__construct
(
array
$params
)
{
foreach
(
[
'KeyPath'
,
'CertPath'
,
'CAFile'
,
'CAPath'
,
'Ciphers'
]
as
$name
)
{
$var
=
"ssl{$name}"
;
if
(
isset
(
$params
[
$var
]
)
)
{
$this
->
$var
=
$params
[
$var
];
}
}
$this
->
utf8Mode
=
!
empty
(
$params
[
'utf8Mode'
]
);
parent
::
__construct
(
$params
);
$this
->
platform
=
new
MySQLPlatform
(
$this
,
$this
->
logger
,
$this
->
currentDomain
,
$this
->
errorLogger
);
$this
->
replicationReporter
=
new
MysqlReplicationReporter
(
$params
[
'topologyRole'
],
$this
->
logger
,
$params
[
'srvCache'
],
$params
[
'lagDetectionMethod'
]
??
'Seconds_Behind_Master'
,
$params
[
'lagDetectionOptions'
]
??
[],
!
empty
(
$params
[
'useGTIDs'
]
)
);
}
/**
* @return string
*/
public
function
getType
()
{
return
'mysql'
;
}
protected
function
open
(
$server
,
$user
,
$password
,
$db
,
$schema
,
$tablePrefix
)
{
$this
->
close
(
__METHOD__
);
if
(
$schema
!==
null
)
{
throw
$this
->
newExceptionAfterConnectError
(
"Got schema '$schema'; not supported."
);
}
$this
->
installErrorHandler
();
try
{
$this
->
conn
=
$this
->
mysqlConnect
(
$server
,
$user
,
$password
,
$db
);
}
catch
(
RuntimeException
$e
)
{
$this
->
restoreErrorHandler
();
throw
$this
->
newExceptionAfterConnectError
(
$e
->
getMessage
()
);
}
$error
=
$this
->
restoreErrorHandler
();
if
(
!
$this
->
conn
)
{
throw
$this
->
newExceptionAfterConnectError
(
$error
?:
$this
->
lastError
()
);
}
try
{
$this
->
currentDomain
=
new
DatabaseDomain
(
(
$db
!==
''
)
?
$db
:
null
,
null
,
$tablePrefix
);
$this
->
platform
->
setCurrentDomain
(
$this
->
currentDomain
);
$set
=
[];
if
(
!
$this
->
flagsHolder
->
getFlag
(
self
::
DBO_GAUGE
)
)
{
// Abstract over any excessive MySQL defaults
$set
[]
=
'group_concat_max_len = 262144'
;
// Set any custom settings defined by site config
// https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
foreach
(
$this
->
connectionVariables
as
$var
=>
$val
)
{
// Escape strings but not numbers to avoid MySQL complaining
if
(
!
is_int
(
$val
)
&&
!
is_float
(
$val
)
)
{
$val
=
$this
->
addQuotes
(
$val
);
}
$set
[]
=
$this
->
platform
->
addIdentifierQuotes
(
$var
)
.
' = '
.
$val
;
}
}
if
(
$set
)
{
$sql
=
'SET '
.
implode
(
', '
,
$set
);
$flags
=
self
::
QUERY_NO_RETRY
|
self
::
QUERY_CHANGE_TRX
;
$query
=
new
Query
(
$sql
,
$flags
,
'SET'
);
// Avoid using query() so that replaceLostConnection() does not throw
// errors if the transaction status is STATUS_TRX_ERROR
$qs
=
$this
->
executeQuery
(
$query
,
__METHOD__
,
$flags
);
if
(
$qs
->
res
===
false
)
{
$this
->
reportQueryError
(
$qs
->
message
,
$qs
->
code
,
$sql
,
__METHOD__
);
}
}
}
catch
(
RuntimeException
$e
)
{
throw
$this
->
newExceptionAfterConnectError
(
$e
->
getMessage
()
);
}
}
protected
function
doSelectDomain
(
DatabaseDomain
$domain
)
{
if
(
$domain
->
getSchema
()
!==
null
)
{
throw
new
DBExpectedError
(
$this
,
__CLASS__
.
": domain '{$domain->getId()}' has a schema component"
);
}
$database
=
$domain
->
getDatabase
();
// A null database means "don't care" so leave it as is and update the table prefix
if
(
$database
===
null
)
{
$this
->
currentDomain
=
new
DatabaseDomain
(
$this
->
currentDomain
->
getDatabase
(),
null
,
$domain
->
getTablePrefix
()
);
$this
->
platform
->
setCurrentDomain
(
$this
->
currentDomain
);
return
true
;
}
if
(
$database
!==
$this
->
getDBname
()
)
{
$sql
=
'USE '
.
$this
->
addIdentifierQuotes
(
$database
);
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_TRX
,
'USE'
);
$qs
=
$this
->
executeQuery
(
$query
,
__METHOD__
,
self
::
QUERY_CHANGE_TRX
);
if
(
$qs
->
res
===
false
)
{
$this
->
reportQueryError
(
$qs
->
message
,
$qs
->
code
,
$sql
,
__METHOD__
);
return
false
;
// unreachable
}
}
// Update that domain fields on success (no exception thrown)
$this
->
currentDomain
=
$domain
;
$this
->
platform
->
setCurrentDomain
(
$domain
);
return
true
;
}
/**
* @return string
*/
public
function
lastError
()
{
if
(
$this
->
conn
)
{
// Even if it's non-zero, it can still be invalid
$error
=
$this
->
mysqlError
(
$this
->
conn
);
if
(
!
$error
)
{
$error
=
$this
->
mysqlError
();
}
}
else
{
$error
=
$this
->
mysqlError
()
?:
$this
->
lastConnectError
;
}
return
$error
;
}
protected
function
isInsertSelectSafe
(
array
$insertOptions
,
array
$selectOptions
,
$fname
)
{
$row
=
$this
->
replicationReporter
->
getReplicationSafetyInfo
(
$this
,
$fname
);
// For row-based-replication, the resulting changes will be relayed, not the query
if
(
$row
->
binlog_format
===
'ROW'
)
{
return
true
;
}
// LIMIT requires ORDER BY on a unique key or it is non-deterministic
if
(
isset
(
$selectOptions
[
'LIMIT'
]
)
)
{
return
false
;
}
// In MySQL, an INSERT SELECT is only replication safe with row-based
// replication or if innodb_autoinc_lock_mode is 0. When those
// conditions aren't met, use non-native mode.
// While we could try to determine if the insert is safe anyway by
// checking if the target table has an auto-increment column that
// isn't set in $varMap, that seems unlikely to be worth the extra
// complexity.
return
(
in_array
(
'NO_AUTO_COLUMNS'
,
$insertOptions
)
||
(
int
)
$row
->
innodb_autoinc_lock_mode
===
0
);
}
protected
function
checkInsertWarnings
(
Query
$query
,
$fname
)
{
if
(
$this
->
conn
&&
$this
->
conn
->
warning_count
)
{
// Yeah it's weird. It's not iterable.
$warnings
=
$this
->
conn
->
get_warnings
();
$done
=
$warnings
===
false
;
while
(
!
$done
)
{
if
(
in_array
(
$warnings
->
errno
,
[
// List based on https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#ignore-effect-on-execution
1048
,
/* ER_BAD_NULL_ERROR */
1526
,
/* ER_NO_PARTITION_FOR_GIVEN_VALUE */
1748
,
/* ER_ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET */
1242
,
/* ER_SUBQUERY_NO_1_ROW */
1369
,
/* ER_VIEW_CHECK_FAILED */
// Truncation and overflow per T108255
1264
,
/* ER_WARN_DATA_OUT_OF_RANGE */
1265
,
/* WARN_DATA_TRUNCATED */
]
)
)
{
$this
->
reportQueryError
(
'Insert returned unacceptable warning: '
.
$warnings
->
message
,
$warnings
->
errno
,
$query
->
getSQL
(),
$fname
);
}
$done
=
!
$warnings
->
next
();
}
}
}
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"
;
}
$options
[
'EXPLAIN'
]
=
true
;
$res
=
$this
->
select
(
$tables
,
$var
,
$conds
,
$fname
,
$options
,
$join_conds
);
if
(
$res
===
false
)
{
return
-
1
;
}
if
(
!
$res
->
numRows
()
)
{
return
0
;
}
$rows
=
1
;
foreach
(
$res
as
$plan
)
{
$rows
*=
$plan
->
rows
>
0
?
$plan
->
rows
:
1
;
// avoid resetting to zero
}
return
(
int
)
$rows
;
}
public
function
tableExists
(
$table
,
$fname
=
__METHOD__
)
{
[
$db
,
$pt
]
=
$this
->
platform
->
getDatabaseAndTableIdentifier
(
$table
);
if
(
isset
(
$this
->
sessionTempTables
[
$db
][
$pt
]
)
)
{
return
true
;
// already known to exist and won't be found in the query anyway
}
return
(
bool
)
$this
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'information_schema.tables'
)
->
where
(
[
'table_schema'
=>
$db
,
'table_name'
=>
$pt
,
]
)
->
caller
(
$fname
)
->
fetchField
();
}
/**
* @param string $table
* @param string $field
* @return MySQLField|false
*/
public
function
fieldInfo
(
$table
,
$field
)
{
$query
=
new
Query
(
"SELECT * FROM "
.
$this
->
tableName
(
$table
)
.
" LIMIT 1"
,
self
::
QUERY_SILENCE_ERRORS
|
self
::
QUERY_IGNORE_DBO_TRX
|
self
::
QUERY_CHANGE_NONE
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
__METHOD__
);
if
(
!
$res
)
{
return
false
;
}
/** @var MysqliResultWrapper $res */
'@phan-var MysqliResultWrapper $res'
;
return
$res
->
getInternalFieldInfo
(
$field
);
}
public
function
indexInfo
(
$table
,
$index
,
$fname
=
__METHOD__
)
{
# https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
$index
=
$this
->
platform
->
indexName
(
$index
);
$query
=
new
Query
(
'SHOW INDEX FROM '
.
$this
->
tableName
(
$table
),
self
::
QUERY_IGNORE_DBO_TRX
|
self
::
QUERY_CHANGE_NONE
,
'SHOW'
);
$res
=
$this
->
query
(
$query
,
$fname
);
foreach
(
$res
as
$row
)
{
if
(
$row
->
Key_name
===
$index
)
{
return
[
'unique'
=>
!
$row
->
Non_unique
];
}
}
return
false
;
}
/**
* @param string $s
* @return string
*/
public
function
strencode
(
$s
)
{
return
$this
->
mysqlRealEscapeString
(
$s
);
}
public
function
serverIsReadOnly
()
{
// Avoid SHOW to avoid internal temporary tables
$flags
=
self
::
QUERY_IGNORE_DBO_TRX
|
self
::
QUERY_CHANGE_NONE
;
$query
=
new
Query
(
"SELECT @@GLOBAL.read_only AS Value"
,
$flags
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
__METHOD__
);
$row
=
$res
->
fetchObject
();
return
$row
&&
(
bool
)
$row
->
Value
;
}
/**
* @return string
*/
public
function
getSoftwareLink
()
{
[
$variant
]
=
$this
->
getMySqlServerVariant
();
if
(
$variant
===
'MariaDB'
)
{
return
'[{{int:version-db-mariadb-url}} MariaDB]'
;
}
return
'[{{int:version-db-mysql-url}} MySQL]'
;
}
/**
* @return string[] (one of ("MariaDB","MySQL"), x.y.z version string)
*/
private
function
getMySqlServerVariant
()
{
$version
=
$this
->
getServerVersion
();
// MariaDB includes its name in its version string; this is how MariaDB's version of
// the mysql command-line client identifies MariaDB servers.
// https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
// https://mariadb.com/kb/en/version/
$parts
=
explode
(
'-'
,
$version
,
2
);
$number
=
$parts
[
0
];
$suffix
=
$parts
[
1
]
??
''
;
if
(
strpos
(
$suffix
,
'MariaDB'
)
!==
false
||
strpos
(
$suffix
,
'-maria-'
)
!==
false
)
{
$vendor
=
'MariaDB'
;
}
else
{
$vendor
=
'MySQL'
;
}
return
[
$vendor
,
$number
];
}
/**
* @return string
*/
public
function
getServerVersion
()
{
// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
// it off (see RPL_VERSION_HACK in include/mysql_com.h).
$version
=
$this
->
conn
->
server_info
;
if
(
str_starts_with
(
$version
,
'5.5.5-'
)
&&
(
str_contains
(
$version
,
'MariaDB'
)
||
str_contains
(
$version
,
'-maria-'
)
)
)
{
$version
=
substr
(
$version
,
strlen
(
'5.5.5-'
)
);
}
return
$version
;
}
/**
* @param array $options
*/
public
function
setSessionOptions
(
array
$options
)
{
$sqlAssignments
=
[];
if
(
isset
(
$options
[
'connTimeout'
]
)
)
{
$encTimeout
=
(
int
)
$options
[
'connTimeout'
];
$sqlAssignments
[]
=
"net_read_timeout=$encTimeout"
;
$sqlAssignments
[]
=
"net_write_timeout=$encTimeout"
;
}
if
(
isset
(
$options
[
'groupConcatMaxLen'
]
)
)
{
$maxLength
=
(
int
)
$options
[
'groupConcatMaxLen'
];
$sqlAssignments
[]
=
"group_concat_max_len=$maxLength"
;
}
if
(
$sqlAssignments
)
{
$query
=
new
Query
(
'SET '
.
implode
(
', '
,
$sqlAssignments
),
self
::
QUERY_CHANGE_TRX
|
self
::
QUERY_CHANGE_NONE
,
'SET'
);
$this
->
query
(
$query
,
__METHOD__
);
}
}
/**
* @param string &$sql
* @param string &$newLine
* @return bool
*/
public
function
streamStatementEnd
(
&
$sql
,
&
$newLine
)
{
if
(
preg_match
(
'/^DELIMITER
\s
+(
\S
+)/i'
,
$newLine
,
$m
)
)
{
$this
->
delimiter
=
$m
[
1
];
$newLine
=
''
;
}
return
parent
::
streamStatementEnd
(
$sql
,
$newLine
);
}
public
function
doLockIsFree
(
string
$lockName
,
string
$method
)
{
$query
=
new
Query
(
$this
->
platform
->
lockIsFreeSQLText
(
$lockName
),
self
::
QUERY_CHANGE_LOCKS
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
$method
);
$row
=
$res
->
fetchObject
();
return
(
$row
->
unlocked
==
1
);
}
public
function
doLock
(
string
$lockName
,
string
$method
,
int
$timeout
)
{
$query
=
new
Query
(
$this
->
platform
->
lockSQLText
(
$lockName
,
$timeout
),
self
::
QUERY_CHANGE_LOCKS
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
$method
);
$row
=
$res
->
fetchObject
();
return
(
$row
->
acquired
!==
null
)
?
(
float
)
$row
->
acquired
:
null
;
}
public
function
doUnlock
(
string
$lockName
,
string
$method
)
{
$query
=
new
Query
(
$this
->
platform
->
unlockSQLText
(
$lockName
),
self
::
QUERY_CHANGE_LOCKS
,
'SELECT'
);
$res
=
$this
->
query
(
$query
,
$method
);
$row
=
$res
->
fetchObject
();
return
(
$row
->
released
==
1
);
}
protected
function
doFlushSession
(
$fname
)
{
// Note that RELEASE_ALL_LOCKS() is not supported well enough to use here.
// https://mariadb.com/kb/en/release_all_locks/
$releaseLockFields
=
[];
foreach
(
$this
->
sessionNamedLocks
as
$name
=>
$info
)
{
$encName
=
$this
->
addQuotes
(
$this
->
platform
->
makeLockName
(
$name
)
);
$releaseLockFields
[]
=
"RELEASE_LOCK($encName)"
;
}
if
(
$releaseLockFields
)
{
$sql
=
'SELECT '
.
implode
(
','
,
$releaseLockFields
);
$flags
=
self
::
QUERY_CHANGE_LOCKS
|
self
::
QUERY_NO_RETRY
;
$query
=
new
Query
(
$sql
,
$flags
,
'SELECT'
);
$qs
=
$this
->
executeQuery
(
$query
,
__METHOD__
,
$flags
);
if
(
$qs
->
res
===
false
)
{
$this
->
reportQueryError
(
$qs
->
message
,
$qs
->
code
,
$sql
,
$fname
,
true
);
}
}
}
public
function
upsert
(
$table
,
array
$rows
,
$uniqueKeys
,
array
$set
,
$fname
=
__METHOD__
)
{
$identityKey
=
$this
->
platform
->
normalizeUpsertParams
(
$uniqueKeys
,
$rows
);
if
(
!
$rows
)
{
return
;
}
$this
->
platform
->
assertValidUpsertSetArray
(
$set
,
$identityKey
,
$rows
);
$encTable
=
$this
->
tableName
(
$table
);
[
$sqlColumns
,
$sqlTuples
]
=
$this
->
platform
->
makeInsertLists
(
$rows
);
$sqlColumnAssignments
=
$this
->
makeList
(
$set
,
self
::
LIST_SET
);
// No need to expose __NEW.* since buildExcludedValue() uses VALUES(column)
// https://mariadb.com/kb/en/insert-on-duplicate-key-update/
// https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
$sql
=
"INSERT INTO $encTable "
.
"($sqlColumns) VALUES $sqlTuples "
.
"ON DUPLICATE KEY UPDATE $sqlColumnAssignments"
;
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_ROWS
,
'INSERT'
,
$table
);
$this
->
query
(
$query
,
$fname
);
// Count updates of conflicting rows and row inserts equally toward the change count
$this
->
lastQueryAffectedRows
=
min
(
$this
->
lastQueryAffectedRows
,
count
(
$rows
)
);
}
public
function
replace
(
$table
,
$uniqueKeys
,
$rows
,
$fname
=
__METHOD__
)
{
$this
->
platform
->
normalizeUpsertParams
(
$uniqueKeys
,
$rows
);
if
(
!
$rows
)
{
return
;
}
$encTable
=
$this
->
tableName
(
$table
);
[
$sqlColumns
,
$sqlTuples
]
=
$this
->
platform
->
makeInsertLists
(
$rows
);
// https://dev.mysql.com/doc/refman/8.0/en/replace.html
$sql
=
"REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples"
;
// Note that any auto-increment columns on conflicting rows will be reassigned
// due to combined DELETE+INSERT semantics. This will be reflected in insertId().
$query
=
new
Query
(
$sql
,
self
::
QUERY_CHANGE_ROWS
,
'REPLACE'
,
$table
);
$this
->
query
(
$query
,
$fname
);
// Do not count deletions of conflicting rows toward the change count
$this
->
lastQueryAffectedRows
=
min
(
$this
->
lastQueryAffectedRows
,
count
(
$rows
)
);
}
protected
function
isConnectionError
(
$errno
)
{
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
// https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
return
in_array
(
$errno
,
[
2013
,
2006
,
2003
,
1927
,
1053
],
true
);
}
protected
function
isQueryTimeoutError
(
$errno
)
{
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
return
in_array
(
$errno
,
[
3024
,
2062
,
1969
,
1028
],
true
);
}
protected
function
isKnownStatementRollbackError
(
$errno
)
{
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
return
in_array
(
$errno
,
[
3024
,
1969
,
1022
,
1062
,
1216
,
1217
,
1137
,
1146
,
1051
,
1054
],
true
);
}
/**
* @param string $oldName
* @param string $newName
* @param bool $temporary
* @param string $fname
* @return bool
*/
public
function
duplicateTableStructure
(
$oldName
,
$newName
,
$temporary
=
false
,
$fname
=
__METHOD__
)
{
$tmp
=
$temporary
?
'TEMPORARY '
:
''
;
$newNameQuoted
=
$this
->
addIdentifierQuotes
(
$newName
);
$oldNameQuoted
=
$this
->
addIdentifierQuotes
(
$oldName
);
$query
=
new
Query
(
"CREATE $tmp TABLE $newNameQuoted (LIKE $oldNameQuoted)"
,
self
::
QUERY_PSEUDO_PERMANENT
|
self
::
QUERY_CHANGE_SCHEMA
,
$temporary
?
'CREATE TEMPORARY'
:
'CREATE'
,
// Use a dot to avoid double-prefixing in Database::getTempTableWrites()
'.'
.
$newName
);
return
$this
->
query
(
$query
,
$fname
);
}
/**
* List all tables on the database
*
* @param string|null $prefix Only show tables with this prefix, e.g. mw_
* @param string $fname Calling function name
* @return array
*/
public
function
listTables
(
$prefix
=
null
,
$fname
=
__METHOD__
)
{
$qb
=
$this
->
newSelectQueryBuilder
()
->
select
(
'table_name'
)
->
from
(
'information_schema.tables'
)
->
where
(
[
'table_schema'
=>
$this
->
currentDomain
->
getDatabase
(),
'table_type'
=>
'BASE TABLE'
]
)
->
caller
(
$fname
);
if
(
$prefix
!==
null
&&
$prefix
!==
''
)
{
$qb
->
andWhere
(
$this
->
expr
(
'table_name'
,
IExpression
::
LIKE
,
new
LikeValue
(
$prefix
,
$this
->
anyString
()
)
)
);
}
return
$qb
->
fetchFieldValues
();
}
public
function
selectSQLText
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
$sql
=
parent
::
selectSQLText
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
);
// https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
// https://mariadb.com/kb/en/library/aborting-statements/
$timeoutMsec
=
intval
(
$options
[
'MAX_EXECUTION_TIME'
]
??
0
);
if
(
$timeoutMsec
>
0
)
{
[
$vendor
,
$number
]
=
$this
->
getMySqlServerVariant
();
if
(
$vendor
===
'MariaDB'
&&
version_compare
(
$number
,
'10.1.2'
,
'>='
)
)
{
$timeoutSec
=
$timeoutMsec
/
1000
;
$sql
=
"SET STATEMENT max_statement_time=$timeoutSec FOR $sql"
;
}
elseif
(
$vendor
===
'MySQL'
&&
version_compare
(
$number
,
'5.7.0'
,
'>='
)
)
{
$sql
=
preg_replace
(
'/^SELECT(?=
\s
)/'
,
"SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/"
,
$sql
);
}
}
return
$sql
;
}
protected
function
doSingleStatementQuery
(
string
$sql
):
QueryStatus
{
$conn
=
$this
->
getBindingHandle
();
// Hide packet warnings caused by things like dropped connections
AtEase
::
suppressWarnings
();
$res
=
$conn
->
query
(
$sql
);
AtEase
::
restoreWarnings
();
// Note that mysqli::insert_id only reflects the last query statement
$insertId
=
(
int
)
$conn
->
insert_id
;
$this
->
lastQueryInsertId
=
$insertId
;
$this
->
sessionLastAutoRowId
=
$insertId
?:
$this
->
sessionLastAutoRowId
;
return
new
QueryStatus
(
$res
instanceof
mysqli_result
?
new
MysqliResultWrapper
(
$this
,
$res
)
:
$res
,
$conn
->
affected_rows
,
$conn
->
error
,
$conn
->
errno
);
}
/**
* @param string|null $server
* @param string|null $user
* @param string|null $password
* @param string|null $db
* @return mysqli|null
* @throws DBConnectionError
*/
private
function
mysqlConnect
(
$server
,
$user
,
$password
,
$db
)
{
if
(
!
function_exists
(
'mysqli_init'
)
)
{
throw
$this
->
newExceptionAfterConnectError
(
"MySQLi functions missing, have you compiled PHP with the --with-mysqli option?"
);
}
// PHP 8.1.0+ throws exceptions by default. Turn that off for consistency.
mysqli_report
(
MYSQLI_REPORT_OFF
);
// Other than mysql_connect, mysqli_real_connect expects an explicit port number
// e.g. "localhost:1234" or "127.0.0.1:1234"
// or Unix domain socket path
// e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar"
// colons are known to be used by Google AppEngine,
// see <https://cloud.google.com/sql/docs/mysql/connect-app-engine>
//
// We need to parse the port or socket path out of $realServer
$port
=
null
;
$socket
=
null
;
$hostAndPort
=
IPUtils
::
splitHostAndPort
(
$server
);
if
(
$hostAndPort
)
{
$realServer
=
$hostAndPort
[
0
];
if
(
$hostAndPort
[
1
]
)
{
$port
=
$hostAndPort
[
1
];
}
}
elseif
(
substr_count
(
$server
,
':/'
)
==
1
)
{
// If we have a colon slash instead of a colon and a port number
// after the ip or hostname, assume it's the Unix domain socket path
[
$realServer
,
$socket
]
=
explode
(
':'
,
$server
,
2
);
}
else
{
$realServer
=
$server
;
}
$mysqli
=
mysqli_init
();
// Make affectedRows() for UPDATE reflect the number of matching rows, regardless
// of whether any column values changed. This is what callers want to know and is
// consistent with what Postgres and SQLite return.
$flags
=
MYSQLI_CLIENT_FOUND_ROWS
;
if
(
$this
->
ssl
)
{
$flags
|=
MYSQLI_CLIENT_SSL
;
$mysqli
->
ssl_set
(
$this
->
sslKeyPath
,
$this
->
sslCertPath
,
$this
->
sslCAFile
,
$this
->
sslCAPath
,
$this
->
sslCiphers
);
}
if
(
$this
->
getFlag
(
self
::
DBO_COMPRESS
)
)
{
$flags
|=
MYSQLI_CLIENT_COMPRESS
;
}
if
(
$this
->
getFlag
(
self
::
DBO_PERSISTENT
)
)
{
$realServer
=
'p:'
.
$realServer
;
}
if
(
$this
->
utf8Mode
)
{
// Tell the server we're communicating with it in UTF-8.
// This may engage various charset conversions.
$mysqli
->
options
(
MYSQLI_SET_CHARSET_NAME
,
'utf8'
);
}
else
{
$mysqli
->
options
(
MYSQLI_SET_CHARSET_NAME
,
'binary'
);
}
$mysqli
->
options
(
MYSQLI_OPT_CONNECT_TIMEOUT
,
$this
->
connectTimeout
?:
3
);
if
(
$this
->
receiveTimeout
)
{
$mysqli
->
options
(
MYSQLI_OPT_READ_TIMEOUT
,
$this
->
receiveTimeout
);
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal socket seems set when used
$ok
=
$mysqli
->
real_connect
(
$realServer
,
$user
,
$password
,
$db
,
$port
,
$socket
,
$flags
);
return
$ok
?
$mysqli
:
null
;
}
protected
function
closeConnection
()
{
return
(
$this
->
conn
instanceof
mysqli
)
?
mysqli_close
(
$this
->
conn
)
:
true
;
}
protected
function
lastInsertId
()
{
return
$this
->
sessionLastAutoRowId
;
}
protected
function
doHandleSessionLossPreconnect
()
{
// https://mariadb.com/kb/en/last_insert_id/
$this
->
sessionLastAutoRowId
=
0
;
}
public
function
insertId
()
{
if
(
$this
->
lastEmulatedInsertId
===
null
)
{
$conn
=
$this
->
getBindingHandle
();
// Note that mysqli::insert_id only reflects the last query statement
$this
->
lastEmulatedInsertId
=
(
int
)
$conn
->
insert_id
;
}
return
$this
->
lastEmulatedInsertId
;
}
/**
* @return int
*/
public
function
lastErrno
()
{
if
(
$this
->
conn
instanceof
mysqli
)
{
return
$this
->
conn
->
errno
;
}
else
{
return
mysqli_connect_errno
();
}
}
/**
* @param mysqli|null $conn Optional connection object
* @return string
*/
private
function
mysqlError
(
$conn
=
null
)
{
if
(
$conn
===
null
)
{
return
(
string
)
mysqli_connect_error
();
}
else
{
return
$conn
->
error
;
}
}
private
function
mysqlRealEscapeString
(
$s
)
{
$conn
=
$this
->
getBindingHandle
();
return
$conn
->
real_escape_string
(
(
string
)
$s
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 06:22 (2 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
4e/e6/73635b8c0f6b1e8b3e3802a50224
Default Alt Text
DatabaseMySQL.php (25 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment