22 use InvalidArgumentException;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\NullLogger;
26 use Wikimedia\Assert\Assert;
32 use Wikimedia\Timestamp\ConvertibleTimestamp;
61 $this->logger =
$logger ??
new NullLogger();
77 public function bitAnd( $fieldLeft, $fieldRight ) {
78 return "($fieldLeft & $fieldRight)";
85 public function bitOr( $fieldLeft, $fieldRight ) {
86 return "($fieldLeft | $fieldRight)";
94 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
126 $fields = is_array( $fields ) ? $fields : [ $fields ];
127 $values = is_array( $values ) ? $values : [ $values ];
130 foreach ( $fields as $alias => $field ) {
131 if ( is_int( $alias ) ) {
134 $encValues[] = $field;
137 foreach ( $values as $value ) {
138 if ( is_int( $value ) || is_float( $value ) ) {
139 $encValues[] = $value;
140 } elseif ( is_string( $value ) ) {
141 $encValues[] = $this->quoter->addQuotes( $value );
142 } elseif ( $value ===
null ) {
149 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
156 foreach ( $a as $field => $value ) {
177 $includeNull =
false;
178 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
180 unset( $value[$nullKey] );
182 if ( count( $value ) == 0 && !$includeNull ) {
183 throw new InvalidArgumentException(
184 __METHOD__ .
": empty input for field $field" );
185 } elseif ( count( $value ) == 0 ) {
187 $list .=
"$field IS NULL";
190 if ( $includeNull ) {
194 if ( count( $value ) == 1 ) {
198 $value = array_values( $value )[0];
199 $list .= $field .
" = " . $this->quoter->addQuotes( $value );
201 $list .= $field .
" IN (" . $this->
makeList( $value ) .
") ";
204 if ( $includeNull ) {
205 $list .=
" OR $field IS NULL)";
208 } elseif ( $value ===
null ) {
210 $list .=
"$field IS ";
212 $list .=
"$field = ";
219 $list .=
"$field = ";
221 $list .= $mode ==
self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
231 foreach ( $data as
$base => $sub ) {
232 if ( count( $sub ) ) {
234 [ $baseKey =>
$base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
249 if ( count( $condsArray ) === 0 ) {
250 throw new InvalidArgumentException(
251 __METHOD__ .
": empty condition array" );
253 $condsByFieldSet = [];
254 foreach ( $condsArray as $conds ) {
255 if ( !count( $conds ) ) {
256 throw new InvalidArgumentException(
257 __METHOD__ .
": empty condition subarray" );
259 $fieldKey = implode(
',', array_keys( $conds ) );
260 $condsByFieldSet[$fieldKey][] = $conds;
263 foreach ( $condsByFieldSet as $conds ) {
264 if ( $result !==
'' ) {
267 $result .= $this->factorCondsWithCommonFields( $conds );
279 private function factorCondsWithCommonFields( $condsArray ) {
280 $first = $condsArray[array_key_first( $condsArray )];
281 if ( count( $first ) === 1 ) {
283 $field = array_key_first( $first );
285 foreach ( $condsArray as $conds ) {
286 $values[] = $conds[$field];
291 $field1 = array_key_first( $first );
292 $nullExpressions = [];
293 $expressionsByField1 = [];
294 foreach ( $condsArray as $conds ) {
295 $value1 = $conds[$field1];
296 unset( $conds[$field1] );
297 if ( $value1 ===
null ) {
298 $nullExpressions[] = $conds;
300 $expressionsByField1[$value1][] = $conds;
306 foreach ( $expressionsByField1 as $value1 => $expressions ) {
307 if ( $result !==
'' ) {
311 $factored = $this->factorCondsWithCommonFields( $expressions );
312 $result .=
"($field1 = " . $this->quoter->addQuotes( $value1 ) .
315 if ( count( $nullExpressions ) ) {
316 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
317 if ( $result !==
'' ) {
321 $result .=
"($field1 IS NULL AND $factored)";
335 return 'CONCAT(' . implode(
',', $stringList ) .
')';
343 if ( !is_numeric( $limit ) ) {
345 "Invalid non-numeric limit passed to " . __METHOD__
351 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
363 [ $escapeChar,
'%',
'_' ],
364 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
374 if ( is_array( $param ) ) {
377 $params = func_get_args();
388 foreach ( $params as $value ) {
390 $s .= $value->toString();
397 $this->quoter->addQuotes(
$s ) .
' ESCAPE ' . $this->quoter->addQuotes( $escapeChar ) .
' ';
421 $glue = $all ?
') UNION ALL (' :
') UNION (';
423 return '(' . implode( $glue, $sqls ) .
')';
430 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
431 if ( is_array( $cond ) ) {
435 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
443 return "REPLACE({$orig}, {$old}, {$new})";
451 $t =
new ConvertibleTimestamp( $ts );
453 return $t->getTimestamp( TS_MW );
457 if ( $ts ===
null ) {
473 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
479 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
483 return ConvertibleTimestamp::convert( $format, $expiry );
492 $functionBody =
"$input FROM $startPosition";
493 if ( $length !==
null ) {
494 $functionBody .=
" FOR $length";
496 return 'SUBSTRING(' . $functionBody .
')';
512 if ( $startPosition === 0 ) {
514 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
516 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
517 throw new InvalidArgumentException(
518 '$startPosition must be a positive integer'
521 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
522 throw new InvalidArgumentException(
523 '$length must be null or an integer greater than or equal to 0'
535 return "CAST( $field AS CHARACTER )";
543 return 'CAST( ' . $field .
' AS INTEGER )';
563 return $this->indexAliases[$index] ?? $index;
571 $this->tableAliases = $aliases;
579 $this->indexAliases = $aliases;
591 $this->currentDomain->getDatabase(),
592 $this->currentDomain->getSchema(),
606 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
608 if ( is_array( $vars ) ) {
614 $options = (array)$options;
615 $useIndexes = ( isset( $options[
'USE INDEX'] ) && is_array( $options[
'USE INDEX'] ) )
616 ? $options[
'USE INDEX']
619 isset( $options[
'IGNORE INDEX'] ) &&
620 is_array( $options[
'IGNORE INDEX'] )
622 ? $options[
'IGNORE INDEX']
626 $this->selectOptionsIncludeLocking( $options ) &&
627 $this->selectFieldsOrOptionsAggregate( $vars, $options )
631 $this->logger->warning(
632 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
636 if ( is_array( $table ) ) {
637 if ( count( $table ) === 0 ) {
642 $table, $useIndexes, $ignoreIndexes, $join_conds );
644 } elseif ( $table !=
'' ) {
647 [ $table ], $useIndexes, $ignoreIndexes, [] );
652 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
655 if ( is_array( $conds ) ) {
659 if ( $conds ===
null || $conds ===
false ) {
660 $this->logger->warning(
664 .
' with incorrect parameters: $conds must be a string or an array',
665 [
'db_log_category' =>
'sql' ]
670 if ( $conds ===
'' || $conds ===
'*' ) {
671 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
672 } elseif ( is_string( $conds ) ) {
673 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
674 "WHERE $conds $preLimitTail";
676 throw new DBLanguageError( __METHOD__ .
' called with incorrect parameters' );
679 if ( isset( $options[
'LIMIT'] ) ) {
680 $sql = $this->
limitResult( $sql, $options[
'LIMIT'],
681 $options[
'OFFSET'] ??
false );
683 $sql =
"$sql $postLimitTail";
685 if ( isset( $options[
'EXPLAIN'] ) ) {
686 $sql =
'EXPLAIN ' . $sql;
696 private function selectOptionsIncludeLocking( $options ) {
697 $options = (array)$options;
698 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
699 if ( in_array( $lock, $options,
true ) ) {
712 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
713 foreach ( (array)$options as $key => $value ) {
714 if ( is_string( $key ) ) {
715 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
718 } elseif ( is_string( $value ) ) {
719 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
725 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
726 foreach ( (array)$fields as $field ) {
727 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
743 foreach ( $fields as $alias => $field ) {
744 if ( is_numeric( $alias ) ) {
763 if ( !$alias || (
string)$alias === (
string)$name ) {
788 $use_index = (array)$use_index;
789 $ignore_index = (array)$ignore_index;
790 $join_conds = (array)$join_conds;
792 foreach ( $tables as $alias => $table ) {
793 if ( !is_string( $alias ) ) {
798 if ( is_array( $table ) ) {
800 if ( count( $table ) > 1 ) {
803 $table, $use_index, $ignore_index, $join_conds ) .
')';
806 $innerTable = reset( $table );
807 $innerAlias = key( $table );
810 is_string( $innerAlias ) ? $innerAlias : $innerTable
818 if ( isset( $join_conds[$alias] ) ) {
819 Assert::parameterType(
'array', $join_conds[$alias],
"join_conds[$alias]" );
820 list( $joinType, $conds ) = $join_conds[$alias];
822 $tableClause .=
' ' . $joinedTable;
823 if ( isset( $use_index[$alias] ) ) {
824 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
826 $tableClause .=
' ' . $use;
829 if ( isset( $ignore_index[$alias] ) ) {
831 implode(
',', (array)$ignore_index[$alias] ) );
832 if ( $ignore !=
'' ) {
833 $tableClause .=
' ' . $ignore;
838 $tableClause .=
' ON (' . $on .
')';
841 $retJOIN[] = $tableClause;
842 } elseif ( isset( $use_index[$alias] ) ) {
844 $tableClause = $joinedTable;
846 implode(
',', (array)$use_index[$alias] )
849 $ret[] = $tableClause;
850 } elseif ( isset( $ignore_index[$alias] ) ) {
852 $tableClause = $joinedTable;
854 implode(
',', (array)$ignore_index[$alias] )
857 $ret[] = $tableClause;
859 $tableClause = $joinedTable;
861 $ret[] = $tableClause;
866 $implicitJoins = implode(
',', $ret );
867 $explicitJoins = implode(
' ', $retJOIN );
870 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
882 switch ( strtoupper( $joinType ) ) {
890 case 'STRAIGHT_JOIN':
891 case 'STRAIGHT JOIN':
912 if ( is_string( $table ) ) {
913 $quotedTable = $this->
tableName( $table );
914 } elseif ( $table instanceof
Subquery ) {
915 $quotedTable = (string)$table;
917 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
920 if ( $alias ===
false || $alias === $table ) {
922 throw new InvalidArgumentException(
"Subquery table missing alias" );
935 public function tableName( $name, $format =
'quoted' ) {
938 __METHOD__ .
': got Subquery instance when expecting a string'
942 # Skip the entire process when we have a string quoted on both ends.
943 # Note that we check the end so that we will still quote any use of
944 # use of `database`.table. But won't break things if someone wants
945 # to query a database table with a dot in the name.
950 # Lets test for any bits of text that should never show up in a table
951 # name. Basically anything like JOIN or ON which are actually part of
952 # SQL queries, but may end up inside of the table value to combine
953 # sql. Such as how the API is doing.
954 # Note that we use a whitespace test rather than a \b test to avoid
955 # any remote case where a word like on may be inside of a table name
956 # surrounded by symbols which may be considered word breaks.
957 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
958 $this->logger->warning(
959 __METHOD__ .
": use of subqueries is not supported this way",
961 'exception' =>
new RuntimeException(),
962 'db_log_category' =>
'sql',
969 # Split database and table into proper variables.
972 # Quote $table and apply the prefix if not quoted.
973 # $tableName might be empty if this is called from Database::replaceVars()
974 $tableName =
"{$prefix}{$table}";
975 if ( $format ===
'quoted'
982 # Quote $schema and $database and merge them with the table name if needed
983 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
984 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
996 # We reverse the explode so that database.table and table both output the correct table.
997 $dbDetails = explode(
'.', $name, 3 );
998 if ( $this->currentDomain ) {
999 $currentDomainPrefix = $this->currentDomain->getTablePrefix();
1001 $currentDomainPrefix =
null;
1003 if ( count( $dbDetails ) == 3 ) {
1004 list( $database, $schema, $table ) = $dbDetails;
1005 # We don't want any prefix added in this case
1007 } elseif ( count( $dbDetails ) == 2 ) {
1008 list( $database, $table ) = $dbDetails;
1009 # We don't want any prefix added in this case
1011 # In dbs that support it, $database may actually be the schema
1012 # but that doesn't affect any of the functionality here
1015 list( $table ) = $dbDetails;
1016 if ( isset( $this->tableAliases[$table] ) ) {
1017 $database = $this->tableAliases[$table][
'dbname'];
1018 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1019 ? $this->tableAliases[$table][
'schema']
1021 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1022 ? $this->tableAliases[$table][
'prefix']
1023 : $currentDomainPrefix;
1027 $prefix = $currentDomainPrefix; # Default prefix
1031 return [ $database, $schema, $prefix, $table ];
1039 if ( $this->currentDomain ) {
1040 return $this->currentDomain->getSchema();
1051 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1052 if ( $namespace !==
null && $namespace !==
'' ) {
1056 $relation = $namespace .
'.' . $relation;
1065 foreach ( $tables as $name ) {
1066 $retVal[$name] = $this->
tableName( $name );
1075 foreach ( $tables as $name ) {
1093 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
1138 $preLimitTail = $postLimitTail =
'';
1143 foreach ( $options as $key => $option ) {
1144 if ( is_numeric( $key ) ) {
1145 $noKeyOptions[$option] =
true;
1153 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1154 $postLimitTail .=
' FOR UPDATE';
1157 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1158 $postLimitTail .=
' LOCK IN SHARE MODE';
1161 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1162 $startOpts .=
'DISTINCT';
1165 # Various MySQL extensions
1166 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1167 $startOpts .=
' /*! STRAIGHT_JOIN */';
1170 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1171 $startOpts .=
' SQL_BIG_RESULT';
1174 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1175 $startOpts .=
' SQL_BUFFER_RESULT';
1178 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1179 $startOpts .=
' SQL_SMALL_RESULT';
1182 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1183 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1186 if ( isset( $options[
'USE INDEX'] ) && is_string( $options[
'USE INDEX'] ) ) {
1191 if ( isset( $options[
'IGNORE INDEX'] ) && is_string( $options[
'IGNORE INDEX'] ) ) {
1197 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1210 if ( isset( $options[
'GROUP BY'] ) ) {
1211 $gb = is_array( $options[
'GROUP BY'] )
1212 ? implode(
',', $options[
'GROUP BY'] )
1213 : $options[
'GROUP BY'];
1214 $sql .=
' GROUP BY ' . $gb;
1216 if ( isset( $options[
'HAVING'] ) ) {
1217 $having = is_array( $options[
'HAVING'] )
1219 : $options[
'HAVING'];
1220 $sql .=
' HAVING ' . $having;
1235 if ( isset( $options[
'ORDER BY'] ) ) {
1236 $ob = is_array( $options[
'ORDER BY'] )
1237 ? implode(
',', $options[
'ORDER BY'] )
1238 : $options[
'ORDER BY'];
1240 return ' ORDER BY ' . $ob;
1249 array $permute_conds,
1251 $fname = __METHOD__,
1257 foreach ( $permute_conds as $field => $values ) {
1262 $values = array_unique( $values );
1264 foreach ( $conds as $cond ) {
1265 foreach ( $values as $value ) {
1266 $cond[$field] = $value;
1267 $newConds[] = $cond;
1273 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
1277 if ( count( $conds ) === 1 &&
1281 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
1290 $limit = $options[
'LIMIT'] ??
null;
1291 $offset = $options[
'OFFSET'] ??
false;
1292 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
1294 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
1296 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
1297 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
1299 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
1303 $options[
'LIMIT'] = $limit + $offset;
1304 unset( $options[
'OFFSET'] );
1309 foreach ( $conds as $cond ) {
1311 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
1315 if ( $limit !==
null ) {
1316 $sql = $this->
limitResult( $sql, $limit, $offset );
1327 $delim, $table, $field, $conds =
'', $join_conds = []
1329 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) .
')';
1331 return '(' . $this->
selectSQLText( $table, $fld, $conds, __METHOD__, [], $join_conds ) .
')';
1335 $table, $vars, $conds =
'', $fname = __METHOD__,
1336 $options = [], $join_conds = []
1339 $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
1347 return "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
1362 $firstRow = $rows[0];
1363 if ( !is_array( $firstRow ) || !$firstRow ) {
1367 $tupleColumns = array_keys( $firstRow );
1370 foreach ( $rows as $row ) {
1371 $rowColumns = array_keys( $row );
1373 if ( $rowColumns !== $tupleColumns ) {
1375 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
1376 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
1383 $magicAliasFields = [];
1384 foreach ( $tupleColumns as $column ) {
1385 $magicAliasFields[] = $aliasPrefix . $column;
1390 implode(
',', $valueTuples ),
1400 return rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
1409 return [
'INSERT IGNORE INTO',
'' ];
1418 array $insertOptions,
1419 array $selectOptions,
1422 list( $sqlVerb, $sqlOpts ) = $this->
isFlagInOptions(
'IGNORE', $insertOptions )
1424 : [
'INSERT INTO',
'' ];
1425 $encDstTable = $this->
tableName( $destTable );
1426 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
1429 array_values( $varMap ),
1436 return rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1446 foreach ( array_keys( $options, $option,
true ) as $k ) {
1447 if ( is_int( $k ) ) {
1465 } elseif ( !$uniqueKey ) {
1469 if ( count( $uniqueKey ) == 1 ) {
1471 $column = reset( $uniqueKey );
1472 $values = array_column( $rows, $column );
1473 if ( count( $values ) !== count( $rows ) ) {
1474 throw new DBLanguageError(
"Missing values for unique key ($column)" );
1480 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
1483 foreach ( $rows as $row ) {
1484 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1485 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1487 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
1493 return count( $orConds ) > 1
1500 throw new DBLanguageError( __METHOD__ .
' called with empty $conds' );
1503 $delTable = $this->
tableName( $delTable );
1504 $joinTable = $this->
tableName( $joinTable );
1505 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1506 if ( $conds !=
'*' ) {
1518 $sql =
"DELETE FROM $table";
1520 if ( $conds !== self::ALL_ROWS ) {
1521 if ( is_array( $conds ) ) {
1524 $sql .=
' WHERE ' . $conds;
1536 if ( $conds && $conds !== self::ALL_ROWS ) {
1537 if ( is_array( $conds ) ) {
1540 $sql .=
' WHERE ' . $conds;
1560 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1561 if ( !$isCondValid ) {
1563 wfDeprecated( $fname .
' called with empty $conds',
'1.35',
false, 4 );
1565 throw new DBLanguageError( $fname .
' called with empty conditions' );
1580 return implode(
' ', $opts );
1595 if ( in_array(
'IGNORE', $options ) ) {
1608 if ( is_array( $options ) ) {
1610 } elseif ( is_string( $options ) ) {
1611 return ( $options ===
'' ) ? [] : [ $options ];
1613 throw new DBLanguageError( __METHOD__ .
': expected string or array' );
1621 return "DROP TABLE " . $this->
tableName( $table ) .
" CASCADE";
1631 '/^\s*(rollback\s+to\s+savepoint|[a-z]+)/i',
1634 ) ? strtoupper( $m[1] ) :
null;
1657 'ROLLBACK TO SAVEPOINT',
1690 $this->
fieldHasBit( $flags, self::QUERY_CHANGE_ROWS ) ||
1691 $this->
fieldHasBit( $flags, self::QUERY_CHANGE_SCHEMA )
1697 $this->
fieldHasBit( $flags, self::QUERY_CHANGE_NONE ) ||
1698 $this->
fieldHasBit( $flags, self::QUERY_CHANGE_TRX ) ||
1699 $this->
fieldHasBit( $flags, self::QUERY_CHANGE_LOCKS )
1706 if ( preg_match(
'/^\s*\(?SELECT\b/i', $sql ) ) {
1707 return (
bool)preg_match(
'/\bFOR\s+UPDATE\)?\s*$/i', $sql );
1723 '/^\s*(BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE)\b/i',
1734 return ( ( $flags & $bit ) === $bit );
1740 return "(SELECT __$column FROM __VALS)";
1779 if ( !$rowOrRows ) {
1781 } elseif ( isset( $rowOrRows[0] ) ) {
1784 $rows = [ $rowOrRows ];
1787 foreach ( $rows as $row ) {
1788 if ( !is_array( $row ) ) {
1790 } elseif ( !$row ) {
1812 if ( !$uniqueKeys ) {
1814 $this->logger->warning(
1815 "upsert/replace called with no unique key",
1817 'exception' =>
new RuntimeException(),
1818 'db_log_category' =>
'sql',
1823 $identityKey = $this->normalizeUpsertKeys( $uniqueKeys );
1824 if ( $identityKey ) {
1826 if ( $allDefaultKeyValues ) {
1829 $this->logger->warning(
1830 "upsert/replace called with all-null values for unique key",
1832 'exception' =>
new RuntimeException(),
1833 'db_log_category' =>
'sql',
1839 return $identityKey;
1849 if ( $conds ===
null || $conds ===
false ) {
1850 $this->logger->warning(
1854 .
' with incorrect parameters: $conds must be a string or an array',
1855 [
'db_log_category' =>
'sql' ]
1858 } elseif ( $conds ===
'' ) {
1862 return is_array( $conds ) ? $conds : [ $conds ];
1871 private function normalizeUpsertKeys( $uniqueKeys ) {
1872 if ( is_string( $uniqueKeys ) ) {
1873 return [ $uniqueKeys ];
1874 } elseif ( !is_array( $uniqueKeys ) ) {
1877 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1878 throw new DBLanguageError(
1879 "The unique key array should contain a single unique index" );
1882 $uniqueKey = $uniqueKeys[0];
1883 if ( is_string( $uniqueKey ) ) {
1886 $this->logger->warning( __METHOD__ .
1887 " called with deprecated parameter style: " .
1888 "the unique key array should be a string or array of string arrays",
1890 'exception' =>
new RuntimeException(),
1891 'db_log_category' =>
'sql',
1894 } elseif ( is_array( $uniqueKey ) ) {
1897 throw new DBLanguageError(
'Invalid unique key array entry' );
1910 foreach ( $rows as $row ) {
1911 foreach ( $identityKey as $column ) {
1912 $numNulls += ( isset( $row[$column] ) ? 0 : 1 );
1918 $numNulls !== ( count( $rows ) * count( $identityKey ) )
1921 "NULL/absent values for unique key (" . implode(
',', $identityKey ) .
")"
1925 return (
bool)$numNulls;
1941 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
1945 foreach ( $set as $k => $v ) {
1946 if ( is_string( $k ) ) {
1948 if ( in_array( $k, $identityKey,
true ) ) {
1949 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1950 $this->logger->warning(
1951 __METHOD__ .
" called with redundant assignment to column '$k'",
1953 'exception' =>
new RuntimeException(),
1954 'db_log_category' =>
'sql',
1959 "Cannot reassign column '$k' since it belongs to identity key"
1963 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1965 if ( in_array( $m[1], $identityKey,
true ) ) {
1967 "Cannot reassign column '{$m[1]}' since it belongs to identity key"
1979 if ( is_array( $var ) ) {
1982 } elseif ( count( $var ) == 1 ) {
1983 $column = $var[0] ?? reset( $var );
1995 $this->schemaVars = is_array( $vars ) ? $vars :
null;
2044 return preg_replace_callback(
2046 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
2047 \'\{\$ (\w+) }\' | # 3. addQuotes
2048 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
2049 /\*\$ (\w+) \*/ # 5. leave unencoded
2051 function ( $m ) use ( $vars ) {
2054 if ( isset( $m[1] ) && $m[1] !==
'' ) {
2055 if ( $m[1] ===
'i' ) {
2060 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
2061 return $this->quoter->addQuotes( $vars[$m[3]] );
2062 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
2064 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
2065 return $vars[$m[5]];
2075 throw new RuntimeException(
'locking must be implemented in subclasses' );
2079 throw new RuntimeException(
'locking must be implemented in subclasses' );
2083 throw new RuntimeException(
'locking must be implemented in subclasses' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Class to handle database/schema/prefix specifications for IDatabase.
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s