Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5988794
SQLPlatform.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
61 KB
Referenced Files
None
Subscribers
None
SQLPlatform.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\Platform
;
use
InvalidArgumentException
;
use
Psr\Log\LoggerInterface
;
use
Psr\Log\NullLogger
;
use
RuntimeException
;
use
Throwable
;
use
Wikimedia\Assert\Assert
;
use
Wikimedia\Rdbms\Database\DbQuoter
;
use
Wikimedia\Rdbms\DatabaseDomain
;
use
Wikimedia\Rdbms\DBLanguageError
;
use
Wikimedia\Rdbms\IExpression
;
use
Wikimedia\Rdbms\LikeMatch
;
use
Wikimedia\Rdbms\LikeValue
;
use
Wikimedia\Rdbms\Query
;
use
Wikimedia\Rdbms\QueryBuilderFromRawSql
;
use
Wikimedia\Rdbms\RawSQLValue
;
use
Wikimedia\Rdbms\Subquery
;
use
Wikimedia\Timestamp\ConvertibleTimestamp
;
/**
* Sql abstraction object.
* This class nor any of its subclasses shouldn't create a db connection.
* It also should not become stateful. The constructor should only rely on addQuotes() method in Database.
* Later that should be replaced with an implementation that doesn't use db connections.
* @since 1.39
*/
class
SQLPlatform
implements
ISQLPlatform
{
/** @var array[] Current map of (table => (dbname, schema, prefix) map) */
protected
$tableAliases
=
[];
/** @var string[] Current map of (index alias => index) */
protected
$indexAliases
=
[];
/** @var DatabaseDomain|null */
protected
$currentDomain
;
/** @var array|null Current variables use for schema element placeholders */
protected
$schemaVars
;
/** @var DbQuoter */
protected
$quoter
;
/** @var LoggerInterface */
protected
$logger
;
/** @var callable Error logging callback */
protected
$errorLogger
;
public
function
__construct
(
DbQuoter
$quoter
,
?
LoggerInterface
$logger
=
null
,
?
DatabaseDomain
$currentDomain
=
null
,
$errorLogger
=
null
)
{
$this
->
quoter
=
$quoter
;
$this
->
logger
=
$logger
??
new
NullLogger
();
$this
->
currentDomain
=
$currentDomain
?:
DatabaseDomain
::
newUnspecified
();
$this
->
errorLogger
=
$errorLogger
??
static
function
(
Throwable
$e
)
{
trigger_error
(
get_class
(
$e
)
.
': '
.
$e
->
getMessage
(),
E_USER_WARNING
);
};
}
public
function
bitNot
(
$field
)
{
return
"(~$field)"
;
}
public
function
bitAnd
(
$fieldLeft
,
$fieldRight
)
{
return
"($fieldLeft & $fieldRight)"
;
}
public
function
bitOr
(
$fieldLeft
,
$fieldRight
)
{
return
"($fieldLeft | $fieldRight)"
;
}
public
function
addIdentifierQuotes
(
$s
)
{
if
(
strcspn
(
$s
,
"
\0\"
`'."
)
!==
strlen
(
$s
)
)
{
throw
new
DBLanguageError
(
"Identifier must not contain quote, dot or null characters"
);
}
$quoteChar
=
$this
->
getIdentifierQuoteChar
();
return
$quoteChar
.
$s
.
$quoteChar
;
}
/**
* Get the character used for identifier quoting
* @return string
*/
protected
function
getIdentifierQuoteChar
()
{
return
'"'
;
}
/**
* @inheritDoc
*/
public
function
buildGreatest
(
$fields
,
$values
)
{
return
$this
->
buildSuperlative
(
'GREATEST'
,
$fields
,
$values
);
}
/**
* @inheritDoc
*/
public
function
buildLeast
(
$fields
,
$values
)
{
return
$this
->
buildSuperlative
(
'LEAST'
,
$fields
,
$values
);
}
/**
* Build a superlative function statement comparing columns/values
*
* Integer and float values in $values will not be quoted
*
* If $fields is an array, then each value with a string key is treated as an expression
* (which must be manually quoted); such string keys do not appear in the SQL and are only
* descriptive aliases.
*
* @param string $sqlfunc Name of a SQL function
* @param string|string[] $fields Name(s) of column(s) with values to compare
* @param string|int|float|string[]|int[]|float[] $values Values to compare
* @return string
*/
protected
function
buildSuperlative
(
$sqlfunc
,
$fields
,
$values
)
{
$fields
=
is_array
(
$fields
)
?
$fields
:
[
$fields
];
$values
=
is_array
(
$values
)
?
$values
:
[
$values
];
$encValues
=
[];
foreach
(
$fields
as
$alias
=>
$field
)
{
if
(
is_int
(
$alias
)
)
{
$encValues
[]
=
$this
->
addIdentifierQuotes
(
$field
);
}
else
{
$encValues
[]
=
$field
;
// expression
}
}
foreach
(
$values
as
$value
)
{
if
(
is_int
(
$value
)
||
is_float
(
$value
)
)
{
$encValues
[]
=
$value
;
}
elseif
(
is_string
(
$value
)
)
{
$encValues
[]
=
$this
->
quoter
->
addQuotes
(
$value
);
}
elseif
(
$value
===
null
)
{
throw
new
DBLanguageError
(
'Null value in superlative'
);
}
else
{
throw
new
DBLanguageError
(
'Unexpected value type in superlative'
);
}
}
return
$sqlfunc
.
'('
.
implode
(
','
,
$encValues
)
.
')'
;
}
public
function
buildComparison
(
string
$op
,
array
$conds
):
string
{
if
(
!
in_array
(
$op
,
[
'>'
,
'>='
,
'<'
,
'<='
]
)
)
{
throw
new
InvalidArgumentException
(
"Comparison operator must be one of '>', '>=', '<', '<='"
);
}
if
(
count
(
$conds
)
===
0
)
{
throw
new
InvalidArgumentException
(
"Empty input"
);
}
// Construct a condition string by starting with the least significant part of the index, and
// adding more significant parts progressively to the left of the string.
//
// For example, given $conds = [ 'a' => 4, 'b' => 7, 'c' => 1 ], this will generate a condition
// like this:
//
// WHERE a > 4
// OR (a = 4 AND (b > 7
// OR (b = 7 AND (c > 1))))
//
// …which is equivalent to the following, which might be easier to understand:
//
// WHERE a > 4
// OR a = 4 AND b > 7
// OR a = 4 AND b = 7 AND c > 1
//
// …and also equivalent to the following, using tuple comparison syntax, which is most intuitive
// but apparently performs worse:
//
// WHERE (a, b, c) > (4, 7, 1)
$sql
=
''
;
foreach
(
array_reverse
(
$conds
)
as
$field
=>
$value
)
{
if
(
is_int
(
$field
)
)
{
throw
new
InvalidArgumentException
(
'Non-associative array passed to buildComparison() (typo?)'
);
}
$encValue
=
$this
->
quoter
->
addQuotes
(
$value
);
if
(
$sql
===
''
)
{
$sql
=
"$field $op $encValue"
;
// Change '>=' to '>' etc. for remaining fields, as the equality is handled separately
$op
=
rtrim
(
$op
,
'='
);
}
else
{
$sql
=
"$field $op $encValue OR ($field = $encValue AND ($sql))"
;
}
}
return
$sql
;
}
public
function
makeList
(
array
$a
,
$mode
=
self
::
LIST_COMMA
)
{
$first
=
true
;
$list
=
''
;
$keyWarning
=
null
;
foreach
(
$a
as
$field
=>
$value
)
{
if
(
$first
)
{
$first
=
false
;
}
else
{
if
(
$mode
==
self
::
LIST_AND
)
{
$list
.=
' AND '
;
}
elseif
(
$mode
==
self
::
LIST_OR
)
{
$list
.=
' OR '
;
}
else
{
$list
.=
','
;
}
}
if
(
(
$mode
==
self
::
LIST_AND
||
$mode
==
self
::
LIST_OR
)
&&
is_numeric
(
$field
)
)
{
if
(
$value
instanceof
IExpression
)
{
$list
.=
"("
.
$value
->
toSql
(
$this
->
quoter
)
.
")"
;
}
elseif
(
is_array
(
$value
)
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": unexpected array value without key"
);
}
elseif
(
$value
instanceof
RawSQLValue
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": unexpected raw value without key"
);
}
else
{
$list
.=
"($value)"
;
}
}
elseif
(
$value
instanceof
IExpression
)
{
if
(
$mode
==
self
::
LIST_AND
||
$mode
==
self
::
LIST_OR
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": unexpected key $field for IExpression value"
);
}
else
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": unexpected IExpression outside WHERE clause"
);
}
}
elseif
(
$mode
==
self
::
LIST_SET
&&
is_numeric
(
$field
)
)
{
$list
.=
"$value"
;
}
elseif
(
(
$mode
==
self
::
LIST_AND
||
$mode
==
self
::
LIST_OR
)
&&
is_array
(
$value
)
)
{
// Remove null from array to be handled separately if found
$includeNull
=
false
;
foreach
(
array_keys
(
$value
,
null
,
true
)
as
$nullKey
)
{
$includeNull
=
true
;
unset
(
$value
[
$nullKey
]
);
}
if
(
count
(
$value
)
==
0
&&
!
$includeNull
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": empty input for field $field"
);
}
elseif
(
count
(
$value
)
==
0
)
{
// only check if $field is null
$list
.=
"$field IS NULL"
;
}
else
{
// IN clause contains at least one valid element
if
(
$includeNull
)
{
// Group subconditions to ensure correct precedence
$list
.=
'('
;
}
if
(
count
(
$value
)
==
1
)
{
// Special-case single values, as IN isn't terribly efficient
// (but call makeList() so that warnings are emitted if needed)
$list
.=
$field
.
" = "
.
$this
->
makeList
(
$value
);
}
else
{
$list
.=
$field
.
" IN ("
.
$this
->
makeList
(
$value
)
.
") "
;
}
// if null present in array, append IS NULL
if
(
$includeNull
)
{
$list
.=
" OR $field IS NULL)"
;
}
}
}
elseif
(
is_array
(
$value
)
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": unexpected nested array"
);
}
elseif
(
$value
===
null
)
{
if
(
$mode
==
self
::
LIST_AND
||
$mode
==
self
::
LIST_OR
)
{
$list
.=
"$field IS "
;
}
elseif
(
$mode
==
self
::
LIST_SET
)
{
$list
.=
"$field = "
;
}
elseif
(
$mode
===
self
::
LIST_COMMA
&&
!
is_numeric
(
$field
)
)
{
$keyWarning
??=
[
__METHOD__
.
": array key {key} in list of values ignored"
,
[
'key'
=>
$field
,
'exception'
=>
new
RuntimeException
()
]
];
}
elseif
(
$mode
===
self
::
LIST_NAMES
&&
!
is_numeric
(
$field
)
)
{
$keyWarning
??=
[
__METHOD__
.
": array key {key} in list of fields ignored"
,
[
'key'
=>
$field
,
'exception'
=>
new
RuntimeException
()
]
];
}
$list
.=
'NULL'
;
}
else
{
if
(
$mode
==
self
::
LIST_AND
||
$mode
==
self
::
LIST_OR
||
$mode
==
self
::
LIST_SET
)
{
$list
.=
"$field = "
;
}
elseif
(
$mode
===
self
::
LIST_COMMA
&&
!
is_numeric
(
$field
)
)
{
$keyWarning
??=
[
__METHOD__
.
": array key {key} in list of values ignored"
,
[
'key'
=>
$field
,
'exception'
=>
new
RuntimeException
()
]
];
}
elseif
(
$mode
===
self
::
LIST_NAMES
&&
!
is_numeric
(
$field
)
)
{
$keyWarning
??=
[
__METHOD__
.
": array key {key} in list of fields ignored"
,
[
'key'
=>
$field
,
'exception'
=>
new
RuntimeException
()
]
];
}
$list
.=
$mode
==
self
::
LIST_NAMES
?
$value
:
$this
->
quoter
->
addQuotes
(
$value
);
}
}
if
(
$keyWarning
)
{
// Only log one warning about this per function call, to reduce log spam when a dynamically
// generated associative array is passed
$this
->
logger
->
warning
(
...
$keyWarning
);
}
return
$list
;
}
public
function
makeWhereFrom2d
(
$data
,
$baseKey
,
$subKey
)
{
$conds
=
[];
foreach
(
$data
as
$base
=>
$sub
)
{
if
(
count
(
$sub
)
)
{
$conds
[]
=
$this
->
makeList
(
[
$baseKey
=>
$base
,
$subKey
=>
array_map
(
'strval'
,
array_keys
(
$sub
)
)
],
self
::
LIST_AND
);
}
}
if
(
!
$conds
)
{
throw
new
InvalidArgumentException
(
"Data for $baseKey and $subKey must be non-empty"
);
}
return
$this
->
makeList
(
$conds
,
self
::
LIST_OR
);
}
public
function
factorConds
(
$condsArray
)
{
if
(
count
(
$condsArray
)
===
0
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": empty condition array"
);
}
$condsByFieldSet
=
[];
foreach
(
$condsArray
as
$conds
)
{
if
(
!
count
(
$conds
)
)
{
throw
new
InvalidArgumentException
(
__METHOD__
.
": empty condition subarray"
);
}
$fieldKey
=
implode
(
','
,
array_keys
(
$conds
)
);
$condsByFieldSet
[
$fieldKey
][]
=
$conds
;
}
$result
=
''
;
foreach
(
$condsByFieldSet
as
$conds
)
{
if
(
$result
!==
''
)
{
$result
.=
' OR '
;
}
$result
.=
$this
->
factorCondsWithCommonFields
(
$conds
);
}
return
$result
;
}
/**
* Same as factorConds() but with each element in the array having the same
* set of array keys. Validation is done by the caller.
*
* @param array $condsArray
* @return string
*/
private
function
factorCondsWithCommonFields
(
$condsArray
)
{
$first
=
$condsArray
[
array_key_first
(
$condsArray
)];
if
(
count
(
$first
)
===
1
)
{
// IN clause
$field
=
array_key_first
(
$first
);
$values
=
[];
foreach
(
$condsArray
as
$conds
)
{
$values
[]
=
$conds
[
$field
];
}
return
$this
->
makeList
(
[
$field
=>
$values
],
self
::
LIST_AND
);
}
$field1
=
array_key_first
(
$first
);
$nullExpressions
=
[];
$expressionsByField1
=
[];
foreach
(
$condsArray
as
$conds
)
{
$value1
=
$conds
[
$field1
];
unset
(
$conds
[
$field1
]
);
if
(
$value1
===
null
)
{
$nullExpressions
[]
=
$conds
;
}
else
{
$expressionsByField1
[
$value1
][]
=
$conds
;
}
}
$wrap
=
false
;
$result
=
''
;
foreach
(
$expressionsByField1
as
$value1
=>
$expressions
)
{
if
(
$result
!==
''
)
{
$result
.=
' OR '
;
$wrap
=
true
;
}
$factored
=
$this
->
factorCondsWithCommonFields
(
$expressions
);
$result
.=
"($field1 = "
.
$this
->
quoter
->
addQuotes
(
$value1
)
.
" AND $factored)"
;
}
if
(
count
(
$nullExpressions
)
)
{
$factored
=
$this
->
factorCondsWithCommonFields
(
$nullExpressions
);
if
(
$result
!==
''
)
{
$result
.=
' OR '
;
$wrap
=
true
;
}
$result
.=
"($field1 IS NULL AND $factored)"
;
}
if
(
$wrap
)
{
return
"($result)"
;
}
else
{
return
$result
;
}
}
/**
* @inheritDoc
* @stable to override
*/
public
function
buildConcat
(
$stringList
)
{
return
'CONCAT('
.
implode
(
','
,
$stringList
)
.
')'
;
}
public
function
limitResult
(
$sql
,
$limit
,
$offset
=
false
)
{
if
(
!
is_numeric
(
$limit
)
)
{
throw
new
DBLanguageError
(
"Invalid non-numeric limit passed to "
.
__METHOD__
);
}
// This version works in MySQL and SQLite. It will very likely need to be
// overridden for most other RDBMS subclasses.
return
"$sql LIMIT "
.
(
(
is_numeric
(
$offset
)
&&
$offset
!=
0
)
?
"{$offset},"
:
""
)
.
"{$limit} "
;
}
/**
* @stable to override
* @param string $s
* @param string $escapeChar
* @return string
*/
public
function
escapeLikeInternal
(
$s
,
$escapeChar
=
'`'
)
{
return
str_replace
(
[
$escapeChar
,
'%'
,
'_'
],
[
"{$escapeChar}{$escapeChar}"
,
"{$escapeChar}%"
,
"{$escapeChar}_"
],
$s
);
}
public
function
buildLike
(
$param
,
...
$params
)
{
if
(
is_array
(
$param
)
)
{
$params
=
$param
;
}
else
{
$params
=
func_get_args
();
}
// @phan-suppress-next-line PhanParamTooFewUnpack
$likeValue
=
new
LikeValue
(
...
$params
);
return
' LIKE '
.
$likeValue
->
toSql
(
$this
->
quoter
);
}
public
function
anyChar
()
{
return
new
LikeMatch
(
'_'
);
}
public
function
anyString
()
{
return
new
LikeMatch
(
'%'
);
}
/**
* @inheritDoc
* @stable to override
*/
public
function
unionSupportsOrderAndLimit
()
{
return
true
;
// True for almost every DB supported
}
public
function
unionQueries
(
$sqls
,
$all
,
$options
=
[]
)
{
$glue
=
$all
?
') UNION ALL ('
:
') UNION ('
;
$sql
=
'('
.
implode
(
$glue
,
$sqls
)
.
')'
;
if
(
!
$this
->
unionSupportsOrderAndLimit
()
)
{
return
$sql
;
}
$sql
.=
$this
->
makeOrderBy
(
$options
);
$limit
=
$options
[
'LIMIT'
]
??
null
;
$offset
=
$options
[
'OFFSET'
]
??
false
;
if
(
$limit
!==
null
)
{
$sql
=
$this
->
limitResult
(
$sql
,
$limit
,
$offset
);
}
return
$sql
;
}
public
function
conditional
(
$cond
,
$caseTrueExpression
,
$caseFalseExpression
)
{
if
(
is_array
(
$cond
)
)
{
$cond
=
$this
->
makeList
(
$cond
,
self
::
LIST_AND
);
}
if
(
$cond
instanceof
IExpression
)
{
$cond
=
$cond
->
toSql
(
$this
->
quoter
);
}
return
"(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)"
;
}
public
function
strreplace
(
$orig
,
$old
,
$new
)
{
return
"REPLACE({$orig}, {$old}, {$new})"
;
}
public
function
timestamp
(
$ts
=
0
)
{
$t
=
new
ConvertibleTimestamp
(
$ts
);
// Let errors bubble up to avoid putting garbage in the DB
return
$t
->
getTimestamp
(
TS_MW
);
}
public
function
timestampOrNull
(
$ts
=
null
)
{
if
(
$ts
===
null
)
{
return
null
;
}
else
{
return
$this
->
timestamp
(
$ts
);
}
}
public
function
getInfinity
()
{
return
'infinity'
;
}
public
function
encodeExpiry
(
$expiry
)
{
return
(
$expiry
==
''
||
$expiry
==
'infinity'
||
$expiry
==
$this
->
getInfinity
()
)
?
$this
->
getInfinity
()
:
$this
->
timestamp
(
$expiry
);
}
public
function
decodeExpiry
(
$expiry
,
$format
=
TS_MW
)
{
if
(
$expiry
==
''
||
$expiry
==
'infinity'
||
$expiry
==
$this
->
getInfinity
()
)
{
return
'infinity'
;
}
return
ConvertibleTimestamp
::
convert
(
$format
,
$expiry
);
}
/**
* @inheritDoc
* @stable to override
*/
public
function
buildSubstring
(
$input
,
$startPosition
,
$length
=
null
)
{
$this
->
assertBuildSubstringParams
(
$startPosition
,
$length
);
$functionBody
=
"$input FROM $startPosition"
;
if
(
$length
!==
null
)
{
$functionBody
.=
" FOR $length"
;
}
return
'SUBSTRING('
.
$functionBody
.
')'
;
}
/**
* Check type and bounds for parameters to self::buildSubstring()
*
* All supported databases have substring functions that behave the same for
* positive $startPosition and non-negative $length, but behaviors differ when
* given negative $startPosition or negative $length. The simplest
* solution to that is to just forbid those values.
*
* @param int $startPosition
* @param int|null $length
* @since 1.31 in Database, moved to SQLPlatform in 1.39
*/
protected
function
assertBuildSubstringParams
(
$startPosition
,
$length
)
{
if
(
$startPosition
===
0
)
{
// The DBMSs we support use 1-based indexing here.
throw
new
InvalidArgumentException
(
'Use 1 as $startPosition for the beginning of the string'
);
}
if
(
!
is_int
(
$startPosition
)
||
$startPosition
<
0
)
{
throw
new
InvalidArgumentException
(
'$startPosition must be a positive integer'
);
}
if
(
!(
(
is_int
(
$length
)
&&
$length
>=
0
)
||
$length
===
null
)
)
{
throw
new
InvalidArgumentException
(
'$length must be null or an integer greater than or equal to 0'
);
}
}
public
function
buildStringCast
(
$field
)
{
// In theory this should work for any standards-compliant
// SQL implementation, although it may not be the best way to do it.
return
"CAST( $field AS CHARACTER )"
;
}
public
function
buildIntegerCast
(
$field
)
{
return
'CAST( '
.
$field
.
' AS INTEGER )'
;
}
public
function
implicitOrderby
()
{
return
true
;
}
/**
* Allows for index remapping in queries where this is not consistent across DBMS
*
* TODO: Make it protected once all the code is moved over.
*
* @param string $index
* @return string
*/
public
function
indexName
(
$index
)
{
return
$this
->
indexAliases
[
$index
]
??
$index
;
}
public
function
setTableAliases
(
array
$aliases
)
{
$this
->
tableAliases
=
$aliases
;
}
public
function
setIndexAliases
(
array
$aliases
)
{
$this
->
indexAliases
=
$aliases
;
}
/**
* @return array[]
*/
public
function
getTableAliases
()
{
return
$this
->
tableAliases
;
}
public
function
setPrefix
(
$prefix
)
{
$this
->
currentDomain
=
new
DatabaseDomain
(
$this
->
currentDomain
->
getDatabase
(),
$this
->
currentDomain
->
getSchema
(),
$prefix
);
}
public
function
setCurrentDomain
(
DatabaseDomain
$currentDomain
)
{
$this
->
currentDomain
=
$currentDomain
;
}
/**
* @internal For use by tests
* @return DatabaseDomain
*/
public
function
getCurrentDomain
()
{
return
$this
->
currentDomain
;
}
public
function
selectSQLText
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
if
(
!
is_array
(
$tables
)
)
{
if
(
$tables
===
''
||
$tables
===
null
||
$tables
===
false
)
{
$tables
=
[];
}
elseif
(
is_string
(
$tables
)
)
{
$tables
=
[
$tables
];
}
else
{
throw
new
DBLanguageError
(
__METHOD__
.
' called with incorrect table parameter'
);
}
}
if
(
is_array
(
$vars
)
)
{
$fields
=
implode
(
','
,
$this
->
fieldNamesWithAlias
(
$vars
)
);
}
else
{
$fields
=
$vars
;
}
$options
=
(
array
)
$options
;
$useIndexByTable
=
$options
[
'USE INDEX'
]
??
[];
if
(
!
is_array
(
$useIndexByTable
)
)
{
if
(
count
(
$tables
)
<=
1
)
{
$useIndexByTable
=
[
reset
(
$tables
)
=>
$useIndexByTable
];
}
else
{
$e
=
new
DBLanguageError
(
__METHOD__
.
" got ambiguous USE INDEX ($fname)"
);
(
$this
->
errorLogger
)(
$e
);
}
}
$ignoreIndexByTable
=
$options
[
'IGNORE INDEX'
]
??
[];
if
(
!
is_array
(
$ignoreIndexByTable
)
)
{
if
(
count
(
$tables
)
<=
1
)
{
$ignoreIndexByTable
=
[
reset
(
$tables
)
=>
$ignoreIndexByTable
];
}
else
{
$e
=
new
DBLanguageError
(
__METHOD__
.
" got ambiguous IGNORE INDEX ($fname)"
);
(
$this
->
errorLogger
)(
$e
);
}
}
if
(
$this
->
selectOptionsIncludeLocking
(
$options
)
&&
$this
->
selectFieldsOrOptionsAggregate
(
$vars
,
$options
)
)
{
// Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
// functions. Discourage use of such queries to encourage compatibility.
$this
->
logger
->
warning
(
__METHOD__
.
": aggregation used with a locking SELECT ($fname)"
);
}
if
(
count
(
$tables
)
)
{
$from
=
' FROM '
.
$this
->
tableNamesWithIndexClauseOrJOIN
(
$tables
,
$useIndexByTable
,
$ignoreIndexByTable
,
$join_conds
);
}
else
{
$from
=
''
;
}
[
$startOpts
,
$preLimitTail
,
$postLimitTail
]
=
$this
->
makeSelectOptions
(
$options
);
if
(
is_array
(
$conds
)
)
{
$where
=
$this
->
makeList
(
$conds
,
self
::
LIST_AND
);
}
elseif
(
$conds
instanceof
IExpression
)
{
$where
=
$conds
->
toSql
(
$this
->
quoter
);
}
elseif
(
$conds
===
null
||
$conds
===
false
)
{
$where
=
''
;
$this
->
logger
->
warning
(
__METHOD__
.
' called from '
.
$fname
.
' with incorrect parameters: $conds must be a string or an array'
,
[
'db_log_category'
=>
'sql'
]
);
}
elseif
(
is_string
(
$conds
)
)
{
$where
=
$conds
;
}
else
{
throw
new
DBLanguageError
(
__METHOD__
.
' called with incorrect parameters'
);
}
// Keep historical extra spaces after FROM to avoid testing failures
if
(
$where
===
''
||
$where
===
'*'
)
{
$sql
=
"SELECT $startOpts $fields $from $preLimitTail"
;
}
else
{
$sql
=
"SELECT $startOpts $fields $from WHERE $where $preLimitTail"
;
}
if
(
isset
(
$options
[
'LIMIT'
]
)
)
{
$sql
=
$this
->
limitResult
(
$sql
,
$options
[
'LIMIT'
],
$options
[
'OFFSET'
]
??
false
);
}
$sql
=
"$sql $postLimitTail"
;
if
(
isset
(
$options
[
'EXPLAIN'
]
)
)
{
$sql
=
'EXPLAIN '
.
$sql
;
}
if
(
$fname
===
static
::
CALLER_UNKNOWN
||
str_starts_with
(
$fname
,
'Wikimedia
\\
Rdbms
\\
'
)
||
$fname
===
'{closure}'
)
{
$exception
=
new
RuntimeException
();
// Try to figure out and report the real caller
$caller
=
''
;
foreach
(
$exception
->
getTrace
()
as
$call
)
{
if
(
str_ends_with
(
$call
[
'file'
]
??
''
,
'Test.php'
)
)
{
// Don't warn when called directly by test code, adding callers there is pointless
break
;
}
elseif
(
str_starts_with
(
$call
[
'class'
]
??
''
,
'Wikimedia
\\
Rdbms
\\
'
)
)
{
// Keep looking for the caller of a rdbms method
}
elseif
(
str_ends_with
(
$call
[
'class'
]
??
''
,
'SelectQueryBuilder'
)
)
{
// Keep looking for the caller of any custom SelectQueryBuilder
}
else
{
// Warn about the external caller we found
$caller
=
implode
(
'::'
,
array_filter
(
[
$call
[
'class'
]
??
null
,
$call
[
'function'
]
]
)
);
break
;
}
}
if
(
$fname
===
'{closure}'
)
{
// Someone did ->caller( __METHOD__ ) in a local function, e.g. in a callback to
// getWithSetCallback(), MWCallableUpdate or doAtomicSection(). That's not very helpful.
// Provide a more specific message. The caller has to be provided like this:
// $method = __METHOD__;
// function ( ... ) use ( $method ) { ... }
$warning
=
"SQL query with incorrect caller (__METHOD__ used inside a closure: {caller}): {sql}"
;
}
else
{
$warning
=
"SQL query did not specify the caller (guessed caller: {caller}): {sql}"
;
}
$this
->
logger
->
warning
(
$warning
,
[
'sql'
=>
$sql
,
'caller'
=>
$caller
,
'exception'
=>
$exception
]
);
}
return
$sql
;
}
/**
* @param string|array $options
* @return bool
*/
private
function
selectOptionsIncludeLocking
(
$options
)
{
$options
=
(
array
)
$options
;
foreach
(
[
'FOR UPDATE'
,
'LOCK IN SHARE MODE'
]
as
$lock
)
{
if
(
in_array
(
$lock
,
$options
,
true
)
)
{
return
true
;
}
}
return
false
;
}
/**
* @param array|string $fields
* @param array|string $options
* @return bool
*/
private
function
selectFieldsOrOptionsAggregate
(
$fields
,
$options
)
{
foreach
(
(
array
)
$options
as
$key
=>
$value
)
{
if
(
is_string
(
$key
)
)
{
if
(
preg_match
(
'/^(?:GROUP BY|HAVING)$/i'
,
$key
)
)
{
return
true
;
}
}
elseif
(
is_string
(
$value
)
)
{
if
(
preg_match
(
'/^(?:DISTINCT|DISTINCTROW)$/i'
,
$value
)
)
{
return
true
;
}
}
}
$regex
=
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)
\s
*
\\
(/i'
;
foreach
(
(
array
)
$fields
as
$field
)
{
if
(
is_string
(
$field
)
&&
preg_match
(
$regex
,
$field
)
)
{
return
true
;
}
}
return
false
;
}
/**
* Gets an array of aliased field names
*
* @param array $fields [ [alias] => field ]
* @return string[] See fieldNameWithAlias()
*/
protected
function
fieldNamesWithAlias
(
$fields
)
{
$retval
=
[];
foreach
(
$fields
as
$alias
=>
$field
)
{
if
(
is_numeric
(
$alias
)
)
{
$alias
=
$field
;
}
$retval
[]
=
$this
->
fieldNameWithAlias
(
$field
,
$alias
);
}
return
$retval
;
}
/**
* Get an aliased field name
* e.g. fieldName AS newFieldName
*
* @stable to override
* @param string $name Field name
* @param string|false $alias Alias (optional)
* @return string SQL name for aliased field. Will not alias a field to its own name
*/
public
function
fieldNameWithAlias
(
$name
,
$alias
=
false
)
{
if
(
!
$alias
||
(
string
)
$alias
===
(
string
)
$name
)
{
return
$name
;
}
else
{
return
$name
.
' AS '
.
$this
->
addIdentifierQuotes
(
$alias
);
// PostgreSQL needs AS
}
}
/**
* Get the aliased table name clause for a FROM clause
* which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
*
* @param array $tables Array of ([alias] => table reference)
* @param array $use_index Same as for select()
* @param array $ignore_index Same as for select()
* @param array $join_conds Same as for select()
* @return string
*/
protected
function
tableNamesWithIndexClauseOrJOIN
(
$tables
,
$use_index
=
[],
$ignore_index
=
[],
$join_conds
=
[]
)
{
$ret
=
[];
$retJOIN
=
[];
$use_index
=
(
array
)
$use_index
;
$ignore_index
=
(
array
)
$ignore_index
;
$join_conds
=
(
array
)
$join_conds
;
foreach
(
$tables
as
$alias
=>
$table
)
{
if
(
!
is_string
(
$alias
)
)
{
// No alias? Set it equal to the table name
$alias
=
$table
;
}
if
(
is_array
(
$table
)
)
{
// A parenthesized group
if
(
count
(
$table
)
>
1
)
{
$joinedTable
=
'('
.
$this
->
tableNamesWithIndexClauseOrJOIN
(
$table
,
$use_index
,
$ignore_index
,
$join_conds
)
.
')'
;
}
else
{
// Degenerate case
$innerTable
=
reset
(
$table
);
$innerAlias
=
key
(
$table
);
$joinedTable
=
$this
->
tableNameWithAlias
(
$innerTable
,
is_string
(
$innerAlias
)
?
$innerAlias
:
$innerTable
);
}
}
else
{
$joinedTable
=
$this
->
tableNameWithAlias
(
$table
,
$alias
);
}
// Is there a JOIN clause for this table?
if
(
isset
(
$join_conds
[
$alias
]
)
)
{
Assert
::
parameterType
(
'array'
,
$join_conds
[
$alias
],
"join_conds[$alias]"
);
[
$joinType
,
$conds
]
=
$join_conds
[
$alias
];
$tableClause
=
$this
->
normalizeJoinType
(
$joinType
);
$tableClause
.=
' '
.
$joinedTable
;
if
(
isset
(
$use_index
[
$alias
]
)
)
{
// has USE INDEX?
$use
=
$this
->
useIndexClause
(
implode
(
','
,
(
array
)
$use_index
[
$alias
]
)
);
if
(
$use
!=
''
)
{
$tableClause
.=
' '
.
$use
;
}
}
if
(
isset
(
$ignore_index
[
$alias
]
)
)
{
// has IGNORE INDEX?
$ignore
=
$this
->
ignoreIndexClause
(
implode
(
','
,
(
array
)
$ignore_index
[
$alias
]
)
);
if
(
$ignore
!=
''
)
{
$tableClause
.=
' '
.
$ignore
;
}
}
$on
=
$this
->
makeList
(
(
array
)
$conds
,
self
::
LIST_AND
);
if
(
$on
!=
''
)
{
$tableClause
.=
' ON ('
.
$on
.
')'
;
}
$retJOIN
[]
=
$tableClause
;
}
elseif
(
isset
(
$use_index
[
$alias
]
)
)
{
// Is there an INDEX clause for this table?
$tableClause
=
$joinedTable
;
$tableClause
.=
' '
.
$this
->
useIndexClause
(
implode
(
','
,
(
array
)
$use_index
[
$alias
]
)
);
$ret
[]
=
$tableClause
;
}
elseif
(
isset
(
$ignore_index
[
$alias
]
)
)
{
// Is there an INDEX clause for this table?
$tableClause
=
$joinedTable
;
$tableClause
.=
' '
.
$this
->
ignoreIndexClause
(
implode
(
','
,
(
array
)
$ignore_index
[
$alias
]
)
);
$ret
[]
=
$tableClause
;
}
else
{
$tableClause
=
$joinedTable
;
$ret
[]
=
$tableClause
;
}
}
// We can't separate explicit JOIN clauses with ',', use ' ' for those
$implicitJoins
=
implode
(
','
,
$ret
);
$explicitJoins
=
implode
(
' '
,
$retJOIN
);
// Compile our final table clause
return
implode
(
' '
,
[
$implicitJoins
,
$explicitJoins
]
);
}
/**
* Validate and normalize a join type
*
* Subclasses may override this to add supported join types.
*
* @param string $joinType
* @return string
*/
protected
function
normalizeJoinType
(
string
$joinType
)
{
switch
(
strtoupper
(
$joinType
)
)
{
case
'JOIN'
:
case
'INNER JOIN'
:
return
'JOIN'
;
case
'LEFT JOIN'
:
return
'LEFT JOIN'
;
case
'STRAIGHT_JOIN'
:
case
'STRAIGHT JOIN'
:
// MySQL only
return
'JOIN'
;
default
:
return
$joinType
;
}
}
/**
* Get an aliased table name
*
* This returns strings like "tableName AS newTableName" for aliased tables
* and "(SELECT * from tableA) newTablename" for subqueries (e.g. derived tables)
*
* @see Database::tableName()
* @param string|Subquery $table The unqualified name of a table, or Subquery
* @param string|false $alias Table alias (optional)
* @return string SQL name for aliased table. Will not alias a table to its own name
*/
protected
function
tableNameWithAlias
(
$table
,
$alias
=
false
)
{
if
(
is_string
(
$table
)
)
{
$quotedTable
=
$this
->
tableName
(
$table
);
}
elseif
(
$table
instanceof
Subquery
)
{
$quotedTable
=
(
string
)
$table
;
}
else
{
throw
new
InvalidArgumentException
(
"Table must be a string or Subquery"
);
}
if
(
$alias
===
false
||
$alias
===
$table
)
{
if
(
$table
instanceof
Subquery
)
{
throw
new
InvalidArgumentException
(
"Subquery table missing alias"
);
}
return
$quotedTable
;
}
else
{
return
$quotedTable
.
' '
.
$this
->
addIdentifierQuotes
(
$alias
);
}
}
public
function
tableName
(
string
$name
,
$format
=
'quoted'
)
{
$prefix
=
$this
->
currentDomain
->
getTablePrefix
();
// Warn about table names that look qualified
if
(
(
str_contains
(
$name
,
'.'
)
&&
!
preg_match
(
'/^information_schema
\.
[a-z_0-9]+$/'
,
$name
)
)
||
(
$prefix
!==
''
&&
str_starts_with
(
$name
,
$prefix
)
)
)
{
$this
->
logger
->
warning
(
__METHOD__
.
' called with qualified table '
.
$name
,
[
'db_log_category'
=>
'sql'
]
);
}
// Extract necessary database, schema, table identifiers and quote them as needed
$formattedComponents
=
[];
foreach
(
$this
->
qualifiedTableComponents
(
$name
)
as
$component
)
{
if
(
$format
===
'quoted'
)
{
$formattedComponents
[]
=
$this
->
addIdentifierQuotes
(
$component
);
}
else
{
$formattedComponents
[]
=
$component
;
}
}
return
implode
(
'.'
,
$formattedComponents
);
}
/**
* Get the table components needed for a query given the currently selected database/schema
*
* The resulting array will take one of the follow forms:
* - <table identifier>
* - <database identifier>.<table identifier> (e.g. non-Postgres)
* - <schema identifier>.<table identifier> (e.g. Postgres-only)
* - <database identifier>.<schema identifier>.<table identifier> (e.g. Postgres-only)
*
* If the provided table name only consists of an unquoted table identifier that has an
* entry in ({@link getTableAliases()}), then, the resulting components will be determined
* from the alias configuration. If such alias configuration does not specify the table
* prefix, then the current DB domain prefix will be prepended to the table identifier.
*
* In all other cases where the provided table name only consists of an unquoted table
* identifier, the current DB domain prefix will be prepended to the table identifier.
*
* Empty database/schema identifiers are omitted from the resulting array.
*
* @param string $name Table name as database.schema.table, database.table, or table
* @return string[] Non-empty list of unquoted identifiers that form the qualified table name
*/
public
function
qualifiedTableComponents
(
$name
)
{
$identifiers
=
$this
->
extractTableNameComponents
(
$name
);
if
(
count
(
$identifiers
)
>
3
)
{
throw
new
DBLanguageError
(
"Too many components in table name '$name'"
);
}
// Table alias config and prefixes only apply to unquoted single-identifier names
if
(
count
(
$identifiers
)
==
1
&&
!
$this
->
isQuotedIdentifier
(
$identifiers
[
0
]
)
)
{
[
$table
]
=
$identifiers
;
if
(
isset
(
$this
->
tableAliases
[
$table
]
)
)
{
// This is an "alias" table that uses a different db/schema/prefix scheme
$database
=
$this
->
tableAliases
[
$table
][
'dbname'
];
$schema
=
is_string
(
$this
->
tableAliases
[
$table
][
'schema'
]
)
?
$this
->
tableAliases
[
$table
][
'schema'
]
:
$this
->
relationSchemaQualifier
();
$prefix
=
is_string
(
$this
->
tableAliases
[
$table
][
'prefix'
]
)
?
$this
->
tableAliases
[
$table
][
'prefix'
]
:
$this
->
currentDomain
->
getTablePrefix
();
}
else
{
// Use the current database domain to resolve the schema and prefix
$database
=
''
;
$schema
=
$this
->
relationSchemaQualifier
();
$prefix
=
$this
->
currentDomain
->
getTablePrefix
();
}
$qualifierIdentifiers
=
[
$database
,
$schema
];
$tableIdentifier
=
$prefix
.
$table
;
}
else
{
$qualifierIdentifiers
=
array_slice
(
$identifiers
,
0
,
-
1
);
$tableIdentifier
=
end
(
$identifiers
);
}
$components
=
[];
foreach
(
$qualifierIdentifiers
as
$identifier
)
{
if
(
$identifier
!==
null
&&
$identifier
!==
''
)
{
$components
[]
=
$this
->
isQuotedIdentifier
(
$identifier
)
?
substr
(
$identifier
,
1
,
-
1
)
:
$identifier
;
}
}
$components
[]
=
$this
->
isQuotedIdentifier
(
$tableIdentifier
)
?
substr
(
$tableIdentifier
,
1
,
-
1
)
:
$tableIdentifier
;
return
$components
;
}
/**
* Extract the dot-separated components of a table name, preserving identifier quotation
*
* @param string $name Table name, possible qualified with db or db+schema
* @return string[] Non-empty list of the identifiers included in the provided table name
*/
public
function
extractTableNameComponents
(
string
$name
)
{
$quoteChar
=
$this
->
getIdentifierQuoteChar
();
$components
=
[];
foreach
(
explode
(
'.'
,
$name
)
as
$component
)
{
if
(
$this
->
isQuotedIdentifier
(
$component
)
)
{
$unquotedComponent
=
substr
(
$component
,
1
,
-
1
);
}
else
{
$unquotedComponent
=
$component
;
}
if
(
str_contains
(
$unquotedComponent
,
$quoteChar
)
)
{
throw
new
DBLanguageError
(
'Table name component contains unexpected quote or dot character'
);
}
$components
[]
=
$component
;
}
return
$components
;
}
/**
* Get the database identifer and prefixed table name identifier for a table
*
* The table name is assumed to be relative to the current DB domain
*
* This method is useful for TEMPORARY table tracking. In MySQL, temp tables with identical
* names can co-exist on different databases, which can be done via CREATE and USE. Note
* that SQLite/PostgreSQL do not allow changing the database within a session. This method
* omits the schema identifier for several reasons:
* - MySQL/MariaDB do not support schemas at all.
* - SQLite/PostgreSQL put all TEMPORARY tables in the same schema (TEMP and pgtemp,
* respectively). When these engines resolve a table reference, they first check for
* a matching table in the temp schema, before checking the current DB domain schema.
* Note that this breaks table segregation based on the schema component of the DB
* domain, e.g. a temp table with unqualified name "x" resolves to the same underlying
* table whether the current DB domain is "my_db-schema1-mw_" or "my_db-schema2-mw_".
* By ignoring the schema, we can at least account for this.
* - Exposing the the TEMP/pg_temp schema here would be too leaky of an abstraction,
* running the risk of unexpected results, such as identifiers that don't match. It is
* easier to just avoid creating identically-named TEMPORARY tables on different schemas.
*
* @internal only to be used inside rdbms library
* @param string $table Table name
* @return array{0:string|null,1:string} (unquoted database name, unquoted prefixed table name)
*/
public
function
getDatabaseAndTableIdentifier
(
string
$table
)
{
$components
=
$this
->
qualifiedTableComponents
(
$table
);
switch
(
count
(
$components
)
)
{
case
1
:
return
[
$this
->
currentDomain
->
getDatabase
(),
$components
[
0
]
];
case
2
:
return
$components
;
default
:
throw
new
DBLanguageError
(
'Too many table components'
);
}
}
/**
* @stable to override
* @return string|null Schema to use to qualify relations in queries
*/
protected
function
relationSchemaQualifier
()
{
return
$this
->
currentDomain
->
getSchema
();
}
/**
* @deprecated since 1.39.
*/
public
function
tableNames
(
...
$tables
)
{
wfDeprecated
(
__METHOD__
,
'1.39'
);
$retVal
=
[];
foreach
(
$tables
as
$name
)
{
$retVal
[
$name
]
=
$this
->
tableName
(
$name
);
}
return
$retVal
;
}
public
function
tableNamesN
(
...
$tables
)
{
$retVal
=
[];
foreach
(
$tables
as
$name
)
{
$retVal
[]
=
$this
->
tableName
(
$name
);
}
return
$retVal
;
}
/**
* Returns if the given identifier looks quoted or not according to
* the database convention for quoting identifiers
*
* @note Do not use this to determine if untrusted input is safe.
* A malicious user can trick this function.
* @param string $name
* @return bool
*/
public
function
isQuotedIdentifier
(
$name
)
{
$quoteChar
=
$this
->
getIdentifierQuoteChar
();
return
strlen
(
$name
)
>
1
&&
$name
[
0
]
===
$quoteChar
&&
$name
[-
1
]
===
$quoteChar
;
}
/**
* USE INDEX clause.
*
* This can be used as optimisation in queries that affect tables with multiple
* indexes if the database does not pick the most optimal one by default.
* The "right" index might vary between database backends and versions thereof,
* as such in practice this is biased toward specifically improving performance
* of large wiki farms that use MySQL or MariaDB (like Wikipedia).
*
* @stable to override
* @param string $index
* @return string
*/
public
function
useIndexClause
(
$index
)
{
return
''
;
}
/**
* IGNORE INDEX clause.
*
* The inverse of Database::useIndexClause.
*
* @param string $index
* @return string
*/
public
function
ignoreIndexClause
(
$index
)
{
return
''
;
}
/**
* Returns an optional USE INDEX clause to go after the table, and a
* string to go at the end of the query.
*
* @see Database::select()
*
* @stable to override
* @param array $options Associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return string[] (START OPTIONS, PRE-LIMIT TAIL, POST-LIMIT TAIL)
*/
protected
function
makeSelectOptions
(
array
$options
)
{
$preLimitTail
=
$postLimitTail
=
''
;
$startOpts
=
''
;
$noKeyOptions
=
[];
foreach
(
$options
as
$key
=>
$option
)
{
if
(
is_numeric
(
$key
)
)
{
$noKeyOptions
[
$option
]
=
true
;
}
}
$preLimitTail
.=
$this
->
makeGroupByWithHaving
(
$options
);
$preLimitTail
.=
$this
->
makeOrderBy
(
$options
);
if
(
isset
(
$noKeyOptions
[
'FOR UPDATE'
]
)
)
{
$postLimitTail
.=
' FOR UPDATE'
;
}
if
(
isset
(
$noKeyOptions
[
'LOCK IN SHARE MODE'
]
)
)
{
$postLimitTail
.=
' LOCK IN SHARE MODE'
;
}
if
(
isset
(
$noKeyOptions
[
'DISTINCT'
]
)
||
isset
(
$noKeyOptions
[
'DISTINCTROW'
]
)
)
{
$startOpts
.=
'DISTINCT'
;
}
# Various MySQL extensions
if
(
isset
(
$noKeyOptions
[
'STRAIGHT_JOIN'
]
)
)
{
$startOpts
.=
' /*! STRAIGHT_JOIN */'
;
}
if
(
isset
(
$noKeyOptions
[
'SQL_BIG_RESULT'
]
)
)
{
$startOpts
.=
' SQL_BIG_RESULT'
;
}
if
(
isset
(
$noKeyOptions
[
'SQL_BUFFER_RESULT'
]
)
)
{
$startOpts
.=
' SQL_BUFFER_RESULT'
;
}
if
(
isset
(
$noKeyOptions
[
'SQL_SMALL_RESULT'
]
)
)
{
$startOpts
.=
' SQL_SMALL_RESULT'
;
}
if
(
isset
(
$noKeyOptions
[
'SQL_CALC_FOUND_ROWS'
]
)
)
{
$startOpts
.=
' SQL_CALC_FOUND_ROWS'
;
}
return
[
$startOpts
,
$preLimitTail
,
$postLimitTail
];
}
/**
* Returns an optional GROUP BY with an optional HAVING
*
* @param array $options Associative array of options
* @return string
* @see Database::select()
* @since 1.21
*/
protected
function
makeGroupByWithHaving
(
$options
)
{
$sql
=
''
;
if
(
isset
(
$options
[
'GROUP BY'
]
)
)
{
$gb
=
is_array
(
$options
[
'GROUP BY'
]
)
?
implode
(
','
,
$options
[
'GROUP BY'
]
)
:
$options
[
'GROUP BY'
];
$sql
.=
' GROUP BY '
.
$gb
;
}
if
(
isset
(
$options
[
'HAVING'
]
)
)
{
$having
=
is_array
(
$options
[
'HAVING'
]
)
?
$this
->
makeList
(
$options
[
'HAVING'
],
self
::
LIST_AND
)
:
$options
[
'HAVING'
];
$sql
.=
' HAVING '
.
$having
;
}
return
$sql
;
}
/**
* Returns an optional ORDER BY
*
* @param array $options Associative array of options
* @return string
* @see Database::select()
* @since 1.21
*/
protected
function
makeOrderBy
(
$options
)
{
if
(
isset
(
$options
[
'ORDER BY'
]
)
)
{
$ob
=
is_array
(
$options
[
'ORDER BY'
]
)
?
implode
(
','
,
$options
[
'ORDER BY'
]
)
:
$options
[
'ORDER BY'
];
return
' ORDER BY '
.
$ob
;
}
return
''
;
}
public
function
buildGroupConcatField
(
$delim
,
$tables
,
$field
,
$conds
=
''
,
$join_conds
=
[]
)
{
$fld
=
"GROUP_CONCAT($field SEPARATOR "
.
$this
->
quoter
->
addQuotes
(
$delim
)
.
')'
;
return
'('
.
$this
->
selectSQLText
(
$tables
,
$fld
,
$conds
,
static
::
CALLER_SUBQUERY
,
[],
$join_conds
)
.
')'
;
}
public
function
buildSelectSubquery
(
$tables
,
$vars
,
$conds
=
''
,
$fname
=
__METHOD__
,
$options
=
[],
$join_conds
=
[]
)
{
return
new
Subquery
(
$this
->
selectSQLText
(
$tables
,
$vars
,
$conds
,
$fname
,
$options
,
$join_conds
)
);
}
public
function
insertSqlText
(
$table
,
array
$rows
)
{
$encTable
=
$this
->
tableName
(
$table
);
[
$sqlColumns
,
$sqlTuples
]
=
$this
->
makeInsertLists
(
$rows
);
return
[
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples"
,
"INSERT INTO $encTable ($sqlColumns) VALUES '?'"
];
}
/**
* Make SQL lists of columns, row tuples, and column aliases for INSERT/VALUES expressions
*
* The tuple column order is that of the columns of the first provided row.
* The provided rows must have exactly the same keys and ordering thereof.
*
* @param array[] $rows Non-empty list of (column => value) maps
* @param string $aliasPrefix Optional prefix to prepend to the magic alias names
* @param string[] $typeByColumn Optional map of (column => data type)
* @return array (comma-separated columns, comma-separated tuples, comma-separated aliases)
* @since 1.35
*/
public
function
makeInsertLists
(
array
$rows
,
$aliasPrefix
=
''
,
array
$typeByColumn
=
[]
)
{
$firstRow
=
$rows
[
0
];
if
(
!
is_array
(
$firstRow
)
||
!
$firstRow
)
{
throw
new
DBLanguageError
(
'Got an empty row list or empty row'
);
}
// List of columns that define the value tuple ordering
$tupleColumns
=
array_keys
(
$firstRow
);
$valueTuples
=
[];
foreach
(
$rows
as
$row
)
{
$rowColumns
=
array_keys
(
$row
);
// VALUES(...) requires a uniform correspondence of (column => value)
if
(
$rowColumns
!==
$tupleColumns
)
{
throw
new
DBLanguageError
(
'Got row columns ('
.
implode
(
', '
,
$rowColumns
)
.
') '
.
'instead of expected ('
.
implode
(
', '
,
$tupleColumns
)
.
')'
);
}
// Make the value tuple that defines this row
$valueTuples
[]
=
'('
.
$this
->
makeList
(
array_values
(
$row
),
self
::
LIST_COMMA
)
.
')'
;
}
$magicAliasFields
=
[];
foreach
(
$tupleColumns
as
$column
)
{
$magicAliasFields
[]
=
$aliasPrefix
.
$column
;
}
return
[
$this
->
makeList
(
$tupleColumns
,
self
::
LIST_NAMES
),
implode
(
','
,
$valueTuples
),
$this
->
makeList
(
$magicAliasFields
,
self
::
LIST_NAMES
)
];
}
public
function
insertNonConflictingSqlText
(
$table
,
array
$rows
)
{
$encTable
=
$this
->
tableName
(
$table
);
[
$sqlColumns
,
$sqlTuples
]
=
$this
->
makeInsertLists
(
$rows
);
[
$sqlVerb
,
$sqlOpts
]
=
$this
->
makeInsertNonConflictingVerbAndOptions
();
return
[
rtrim
(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts"
),
rtrim
(
"$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts"
)
];
}
/**
* @stable to override
* @return string[] ("INSERT"-style SQL verb, "ON CONFLICT"-style clause or "")
* @since 1.35
*/
protected
function
makeInsertNonConflictingVerbAndOptions
()
{
return
[
'INSERT IGNORE INTO'
,
''
];
}
public
function
insertSelectNativeSqlText
(
$destTable
,
$srcTable
,
array
$varMap
,
$conds
,
$fname
,
array
$insertOptions
,
array
$selectOptions
,
$selectJoinConds
)
{
[
$sqlVerb
,
$sqlOpts
]
=
$this
->
isFlagInOptions
(
'IGNORE'
,
$insertOptions
)
?
$this
->
makeInsertNonConflictingVerbAndOptions
()
:
[
'INSERT INTO'
,
''
];
$encDstTable
=
$this
->
tableName
(
$destTable
);
$sqlDstColumns
=
implode
(
','
,
array_keys
(
$varMap
)
);
$selectSql
=
$this
->
selectSQLText
(
$srcTable
,
array_values
(
$varMap
),
$conds
,
$fname
,
$selectOptions
,
$selectJoinConds
);
return
rtrim
(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts"
);
}
/**
* @param string $option Query option flag (e.g. "IGNORE" or "FOR UPDATE")
* @param array $options Combination option/value map and boolean option list
* @return bool Whether the option appears as an integer-keyed value in the options
* @since 1.35
*/
public
function
isFlagInOptions
(
$option
,
array
$options
)
{
foreach
(
array_keys
(
$options
,
$option
,
true
)
as
$k
)
{
if
(
is_int
(
$k
)
)
{
return
true
;
}
}
return
false
;
}
/**
* Build an SQL condition to find rows with matching key values to those in $rows.
*
* @param array[] $rows Non-empty list of rows
* @param string[] $uniqueKey List of columns that define a single unique index
* @return string
*/
public
function
makeKeyCollisionCondition
(
array
$rows
,
array
$uniqueKey
)
{
if
(
!
$rows
)
{
throw
new
DBLanguageError
(
"Empty row array"
);
}
elseif
(
!
$uniqueKey
)
{
throw
new
DBLanguageError
(
"Empty unique key array"
);
}
if
(
count
(
$uniqueKey
)
==
1
)
{
// Use a simple IN(...) clause
$column
=
reset
(
$uniqueKey
);
$values
=
array_column
(
$rows
,
$column
);
if
(
count
(
$values
)
!==
count
(
$rows
)
)
{
throw
new
DBLanguageError
(
"Missing values for unique key ($column)"
);
}
return
$this
->
makeList
(
[
$column
=>
$values
],
self
::
LIST_AND
);
}
$nullByUniqueKeyColumn
=
array_fill_keys
(
$uniqueKey
,
null
);
$orConds
=
[];
foreach
(
$rows
as
$row
)
{
$rowKeyMap
=
array_intersect_key
(
$row
,
$nullByUniqueKeyColumn
);
if
(
count
(
$rowKeyMap
)
!=
count
(
$uniqueKey
)
)
{
throw
new
DBLanguageError
(
"Missing values for unique key ("
.
implode
(
','
,
$uniqueKey
)
.
")"
);
}
$orConds
[]
=
$this
->
makeList
(
$rowKeyMap
,
self
::
LIST_AND
);
}
return
count
(
$orConds
)
>
1
?
$this
->
makeList
(
$orConds
,
self
::
LIST_OR
)
:
$orConds
[
0
];
}
public
function
deleteJoinSqlText
(
$delTable
,
$joinTable
,
$delVar
,
$joinVar
,
$conds
)
{
if
(
!
$conds
)
{
throw
new
DBLanguageError
(
__METHOD__
.
' called with empty $conds'
);
}
$delTable
=
$this
->
tableName
(
$delTable
);
$joinTable
=
$this
->
tableName
(
$joinTable
);
$sql
=
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "
;
if
(
$conds
!=
'*'
)
{
$sql
.=
'WHERE '
.
$this
->
makeList
(
$conds
,
self
::
LIST_AND
);
}
$sql
.=
')'
;
return
$sql
;
}
/**
* @param string $table The unqualified name of a table
* @param string|array $conds
* @return Query
*/
public
function
deleteSqlText
(
$table
,
$conds
)
{
$isCondValid
=
(
is_string
(
$conds
)
||
is_array
(
$conds
)
)
&&
$conds
;
if
(
!
$isCondValid
)
{
throw
new
DBLanguageError
(
__METHOD__
.
' called with empty conditions'
);
}
$encTable
=
$this
->
tableName
(
$table
);
$sql
=
"DELETE FROM $encTable"
;
$condsSql
=
''
;
$cleanCondsSql
=
''
;
if
(
$conds
!==
self
::
ALL_ROWS
&&
$conds
!==
[
self
::
ALL_ROWS
]
)
{
$cleanCondsSql
=
' WHERE '
.
$this
->
scrubArray
(
$conds
);
if
(
is_array
(
$conds
)
)
{
$conds
=
$this
->
makeList
(
$conds
,
self
::
LIST_AND
);
}
$condsSql
.=
' WHERE '
.
$conds
;
}
return
new
Query
(
$sql
.
$condsSql
,
self
::
QUERY_CHANGE_ROWS
,
'DELETE'
,
$table
,
$sql
.
$cleanCondsSql
);
}
private
function
scrubArray
(
$array
,
$listType
=
self
::
LIST_AND
)
{
if
(
is_array
(
$array
)
)
{
$scrubbedArray
=
[];
foreach
(
$array
as
$key
=>
$value
)
{
if
(
$value
instanceof
IExpression
)
{
$scrubbedArray
[
$key
]
=
$value
->
toGeneralizedSql
();
}
else
{
$scrubbedArray
[
$key
]
=
'?'
;
}
}
return
$this
->
makeList
(
$scrubbedArray
,
$listType
);
}
return
'?'
;
}
public
function
updateSqlText
(
$table
,
$set
,
$conds
,
$options
)
{
$isCondValid
=
(
is_string
(
$conds
)
||
is_array
(
$conds
)
)
&&
$conds
;
if
(
!
$isCondValid
)
{
throw
new
DBLanguageError
(
__METHOD__
.
' called with empty conditions'
);
}
$encTable
=
$this
->
tableName
(
$table
);
$opts
=
$this
->
makeUpdateOptions
(
$options
);
$sql
=
"UPDATE $opts $encTable"
;
$condsSql
=
" SET "
.
$this
->
makeList
(
$set
,
self
::
LIST_SET
);
$cleanCondsSql
=
" SET "
.
$this
->
scrubArray
(
$set
,
self
::
LIST_SET
);
if
(
$conds
&&
$conds
!==
self
::
ALL_ROWS
&&
$conds
!==
[
self
::
ALL_ROWS
]
)
{
$cleanCondsSql
.=
' WHERE '
.
$this
->
scrubArray
(
$conds
);
if
(
is_array
(
$conds
)
)
{
$conds
=
$this
->
makeList
(
$conds
,
self
::
LIST_AND
);
}
$condsSql
.=
' WHERE '
.
$conds
;
}
return
new
Query
(
$sql
.
$condsSql
,
self
::
QUERY_CHANGE_ROWS
,
'UPDATE'
,
$table
,
$sql
.
$cleanCondsSql
);
}
/**
* Make UPDATE options for the Database::update function
*
* @param array $options The options passed to Database::update
* @return string
*/
protected
function
makeUpdateOptions
(
$options
)
{
$opts
=
$this
->
makeUpdateOptionsArray
(
$options
);
return
implode
(
' '
,
$opts
);
}
/**
* Make UPDATE options array for Database::makeUpdateOptions
*
* @stable to override
* @param array $options
* @return array
*/
protected
function
makeUpdateOptionsArray
(
$options
)
{
$options
=
$this
->
normalizeOptions
(
$options
);
$opts
=
[];
if
(
in_array
(
'IGNORE'
,
$options
)
)
{
$opts
[]
=
'IGNORE'
;
}
return
$opts
;
}
/**
* @param string|array $options
* @return array Combination option/value map and boolean option list
* @since 1.35, moved to SQLPlatform in 1.39
*/
final
public
function
normalizeOptions
(
$options
)
{
if
(
is_array
(
$options
)
)
{
return
$options
;
}
elseif
(
is_string
(
$options
)
)
{
return
(
$options
===
''
)
?
[]
:
[
$options
];
}
else
{
throw
new
DBLanguageError
(
__METHOD__
.
': expected string or array'
);
}
}
public
function
dropTableSqlText
(
$table
)
{
// https://mariadb.com/kb/en/drop-table/
// https://dev.mysql.com/doc/refman/8.0/en/drop-table.html
// https://www.postgresql.org/docs/9.2/sql-truncate.html
return
"DROP TABLE "
.
$this
->
tableName
(
$table
)
.
" CASCADE"
;
}
/**
* @param string $sql SQL query
* @return string|null
* @deprecated Since 1.42
*/
public
function
getQueryVerb
(
$sql
)
{
wfDeprecated
(
__METHOD__
,
'1.42'
);
return
QueryBuilderFromRawSql
::
buildQuery
(
$sql
,
0
)->
getVerb
();
}
/**
* Determine whether a SQL statement is sensitive to isolation level.
*
* A SQL statement is considered transactable if its result could vary
* depending on the transaction isolation level. Operational commands
* such as 'SET' and 'SHOW' are not considered to be transactable.
*
* Main purpose: Used by query() to decide whether to begin a transaction
* before the current query (in DBO_TRX mode, on by default).
*
* @return bool
*/
public
function
isTransactableQuery
(
Query
$sql
)
{
return
!
in_array
(
$sql
->
getVerb
(),
[
'BEGIN'
,
'ROLLBACK'
,
'ROLLBACK TO SAVEPOINT'
,
'COMMIT'
,
'SET'
,
'SHOW'
,
'CREATE'
,
'ALTER'
,
'USE'
,
'SHOW'
],
true
);
}
public
function
buildExcludedValue
(
$column
)
{
/* @see Database::upsert() */
// This can be treated like a single value since __VALS is a single row table
return
"(SELECT __$column FROM __VALS)"
;
}
public
function
savepointSqlText
(
$identifier
)
{
return
'SAVEPOINT '
.
$this
->
addIdentifierQuotes
(
$identifier
);
}
public
function
releaseSavepointSqlText
(
$identifier
)
{
return
'RELEASE SAVEPOINT '
.
$this
->
addIdentifierQuotes
(
$identifier
);
}
public
function
rollbackToSavepointSqlText
(
$identifier
)
{
return
'ROLLBACK TO SAVEPOINT '
.
$this
->
addIdentifierQuotes
(
$identifier
);
}
public
function
rollbackSqlText
()
{
return
'ROLLBACK'
;
}
public
function
dispatchingInsertSqlText
(
$table
,
$rows
,
$options
)
{
$rows
=
$this
->
normalizeRowArray
(
$rows
);
if
(
!
$rows
)
{
return
false
;
}
$options
=
$this
->
normalizeOptions
(
$options
);
if
(
$this
->
isFlagInOptions
(
'IGNORE'
,
$options
)
)
{
[
$sql
,
$cleanSql
]
=
$this
->
insertNonConflictingSqlText
(
$table
,
$rows
);
}
else
{
[
$sql
,
$cleanSql
]
=
$this
->
insertSqlText
(
$table
,
$rows
);
}
return
new
Query
(
$sql
,
self
::
QUERY_CHANGE_ROWS
,
'INSERT'
,
$table
,
$cleanSql
);
}
/**
* @param array $rowOrRows A single (field => value) map or a list of such maps
* @return array[] List of (field => value) maps
* @since 1.35
*/
final
protected
function
normalizeRowArray
(
array
$rowOrRows
)
{
if
(
!
$rowOrRows
)
{
$rows
=
[];
}
elseif
(
isset
(
$rowOrRows
[
0
]
)
)
{
$rows
=
$rowOrRows
;
}
else
{
$rows
=
[
$rowOrRows
];
}
foreach
(
$rows
as
$row
)
{
if
(
!
is_array
(
$row
)
)
{
throw
new
DBLanguageError
(
"Got non-array in row array"
);
}
elseif
(
!
$row
)
{
throw
new
DBLanguageError
(
"Got empty array in row array"
);
}
}
return
$rows
;
}
/**
* Validate and normalize parameters to upsert() or replace()
*
* @param string|string[]|string[][] $uniqueKeys Unique indexes (only one is allowed)
* @param array[] &$rows The row array, which will be replaced with a normalized version.
* @return string[] List of columns that defines a single unique index
* @since 1.35
*/
final
public
function
normalizeUpsertParams
(
$uniqueKeys
,
&
$rows
)
{
$rows
=
$this
->
normalizeRowArray
(
$rows
);
if
(
!
$uniqueKeys
)
{
throw
new
DBLanguageError
(
'No unique key specified for upsert/replace'
);
}
$uniqueKey
=
$this
->
normalizeUpsertKeys
(
$uniqueKeys
);
$this
->
assertValidUpsertRowArray
(
$rows
,
$uniqueKey
);
return
$uniqueKey
;
}
/**
* @param array|string $conds
* @param string $fname
* @return array
* @since 1.31
*/
final
public
function
normalizeConditions
(
$conds
,
$fname
)
{
if
(
$conds
===
null
||
$conds
===
false
)
{
$this
->
logger
->
warning
(
__METHOD__
.
' called from '
.
$fname
.
' with incorrect parameters: $conds must be a string or an array'
,
[
'db_log_category'
=>
'sql'
]
);
return
[];
}
elseif
(
$conds
===
''
)
{
return
[];
}
return
is_array
(
$conds
)
?
$conds
:
[
$conds
];
}
/**
* @param string|string[]|string[][] $uniqueKeys Unique indexes (only one is allowed)
* @return string[] List of columns that defines a single unique index
* @since 1.35
*/
private
function
normalizeUpsertKeys
(
$uniqueKeys
)
{
if
(
is_string
(
$uniqueKeys
)
)
{
return
[
$uniqueKeys
];
}
elseif
(
!
is_array
(
$uniqueKeys
)
)
{
throw
new
DBLanguageError
(
'Invalid unique key array'
);
}
else
{
if
(
count
(
$uniqueKeys
)
!==
1
||
!
isset
(
$uniqueKeys
[
0
]
)
)
{
throw
new
DBLanguageError
(
"The unique key array should contain a single unique index"
);
}
$uniqueKey
=
$uniqueKeys
[
0
];
if
(
is_string
(
$uniqueKey
)
)
{
// Passing a list of strings for single-column unique keys is too
// easily confused with passing the columns of composite unique key
$this
->
logger
->
warning
(
__METHOD__
.
" called with deprecated parameter style: "
.
"the unique key array should be a string or array of string arrays"
,
[
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'sql'
,
]
);
return
$uniqueKeys
;
}
elseif
(
is_array
(
$uniqueKey
)
)
{
return
$uniqueKey
;
}
else
{
throw
new
DBLanguageError
(
'Invalid unique key array entry'
);
}
}
}
/**
* @param array<int,array> $rows Normalized list of rows to insert
* @param string[] $uniqueKey Columns of the unique key to UPSERT upon
* @since 1.37
*/
final
protected
function
assertValidUpsertRowArray
(
array
$rows
,
array
$uniqueKey
)
{
foreach
(
$rows
as
$row
)
{
foreach
(
$uniqueKey
as
$column
)
{
if
(
!
isset
(
$row
[
$column
]
)
)
{
throw
new
DBLanguageError
(
"NULL/absent values for unique key ("
.
implode
(
','
,
$uniqueKey
)
.
")"
);
}
}
}
}
/**
* @param array $set Combined column/literal assignment map and SQL assignment list
* @param string[] $uniqueKey Columns of the unique key to UPSERT upon
* @param array<int,array> $rows List of rows to upsert
* @since 1.37
*/
final
public
function
assertValidUpsertSetArray
(
array
$set
,
array
$uniqueKey
,
array
$rows
)
{
if
(
!
$set
)
{
throw
new
DBLanguageError
(
"Update assignment list can't be empty for upsert"
);
}
// Sloppy callers might construct the SET array using the ROW array, leaving redundant
// column definitions for unique key columns. Detect this for backwards compatibility.
$soleRow
=
(
count
(
$rows
)
==
1
)
?
reset
(
$rows
)
:
null
;
// Disallow value changes for any columns in the unique key. This avoids additional
// insertion order dependencies that are unwieldy and difficult to implement efficiently
// in PostgreSQL.
foreach
(
$set
as
$k
=>
$v
)
{
if
(
is_string
(
$k
)
)
{
// Key is a column name and value is a literal (e.g. string, int, null, ...)
if
(
in_array
(
$k
,
$uniqueKey
,
true
)
)
{
if
(
$soleRow
&&
array_key_exists
(
$k
,
$soleRow
)
&&
$soleRow
[
$k
]
===
$v
)
{
$this
->
logger
->
warning
(
__METHOD__
.
" called with redundant assignment to column '$k'"
,
[
'exception'
=>
new
RuntimeException
(),
'db_log_category'
=>
'sql'
,
]
);
}
else
{
throw
new
DBLanguageError
(
"Cannot reassign column '$k' since it belongs to the provided unique key"
);
}
}
}
elseif
(
preg_match
(
'/^([a-zA-Z0-9_]+)
\s
*=/'
,
$v
,
$m
)
)
{
// Value is of the form "<unquoted alphanumeric column> = <SQL expression>"
if
(
in_array
(
$m
[
1
],
$uniqueKey
,
true
)
)
{
throw
new
DBLanguageError
(
"Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
);
}
}
}
}
/**
* @param array|string $var Field parameter in the style of select()
* @return string|null Column name or null; ignores aliases
*/
final
public
function
extractSingleFieldFromList
(
$var
)
{
if
(
is_array
(
$var
)
)
{
if
(
!
$var
)
{
$column
=
null
;
}
elseif
(
count
(
$var
)
==
1
)
{
$column
=
$var
[
0
]
??
reset
(
$var
);
}
else
{
throw
new
DBLanguageError
(
__METHOD__
.
': got multiple columns'
);
}
}
else
{
$column
=
$var
;
}
return
$column
;
}
public
function
setSchemaVars
(
$vars
)
{
$this
->
schemaVars
=
is_array
(
$vars
)
?
$vars
:
null
;
}
/**
* Get schema variables. If none have been set via setSchemaVars(), then
* use some defaults from the current object.
*
* @return array
*/
protected
function
getSchemaVars
()
{
return
$this
->
schemaVars
??
$this
->
getDefaultSchemaVars
();
}
/**
* Get schema variables to use if none have been set via setSchemaVars().
*
* Override this in derived classes to provide variables for tables.sql
* and SQL patch files.
*
* @stable to override
* @return array
*/
protected
function
getDefaultSchemaVars
()
{
return
[];
}
/**
* Database-independent variable replacement. Replaces a set of variables
* in an SQL statement with their contents as given by $this->getSchemaVars().
*
* Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
*
* - '{$var}' should be used for text and is passed through the database's
* addQuotes method.
* - `{$var}` should be used for identifiers (e.g. table and database names).
* It is passed through the database's addIdentifierQuotes method which
* can be overridden if the database uses something other than backticks.
* - / *_* / or / *$wgDBprefix* / passes the name that follows through the
* database's tableName method.
* - / *i* / passes the name that follows through the database's indexName method.
* - In all other cases, / *$var* / is left unencoded. Except for table options,
* its use should be avoided. In 1.24 and older, string encoding was applied.
*
* @param string $ins SQL statement to replace variables in
* @return string The new SQL statement with variables replaced
*/
public
function
replaceVars
(
$ins
)
{
$vars
=
$this
->
getSchemaVars
();
return
preg_replace_callback
(
'!
/
\*
(
\$
wgDBprefix|[_i])
\*
/ (
\w
*) | # 1-2. tableName, indexName
\'\{\$
(
\w
+) }
\'
| # 3. addQuotes
`
\{\$
(
\w
+) }` | # 4. addIdentifierQuotes
/
\*\$
(
\w
+)
\*
/ # 5. leave unencoded
!x'
,
function
(
$m
)
use
(
$vars
)
{
// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
// check for both nonexistent keys *and* the empty string.
if
(
isset
(
$m
[
1
]
)
&&
$m
[
1
]
!==
''
)
{
if
(
$m
[
1
]
===
'i'
)
{
return
$this
->
indexName
(
$m
[
2
]
);
}
else
{
return
$this
->
tableName
(
$m
[
2
]
);
}
}
elseif
(
isset
(
$m
[
3
]
)
&&
$m
[
3
]
!==
''
&&
array_key_exists
(
$m
[
3
],
$vars
)
)
{
return
$this
->
quoter
->
addQuotes
(
$vars
[
$m
[
3
]]
);
}
elseif
(
isset
(
$m
[
4
]
)
&&
$m
[
4
]
!==
''
&&
array_key_exists
(
$m
[
4
],
$vars
)
)
{
return
$this
->
addIdentifierQuotes
(
$vars
[
$m
[
4
]]
);
}
elseif
(
isset
(
$m
[
5
]
)
&&
$m
[
5
]
!==
''
&&
array_key_exists
(
$m
[
5
],
$vars
)
)
{
return
$vars
[
$m
[
5
]];
}
else
{
return
$m
[
0
];
}
},
$ins
);
}
public
function
lockSQLText
(
$lockName
,
$timeout
)
{
throw
new
RuntimeException
(
'locking must be implemented in subclasses'
);
}
public
function
lockIsFreeSQLText
(
$lockName
)
{
throw
new
RuntimeException
(
'locking must be implemented in subclasses'
);
}
public
function
unlockSQLText
(
$lockName
)
{
throw
new
RuntimeException
(
'locking must be implemented in subclasses'
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Oct 4, 00:55 (3 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d9/22/28a53fcc31fb77ce2740bd2e738b
Default Alt Text
SQLPlatform.php (61 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment