22use InvalidArgumentException;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
27use Wikimedia\Assert\Assert;
38use Wikimedia\Timestamp\ConvertibleTimestamp;
60 ?LoggerInterface
$logger =
null,
65 $this->logger =
$logger ??
new NullLogger();
67 $this->errorLogger =
$errorLogger ??
static function ( Throwable $e ) {
68 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
76 public function bitAnd( $fieldLeft, $fieldRight ) {
77 return "($fieldLeft & $fieldRight)";
80 public function bitOr( $fieldLeft, $fieldRight ) {
81 return "($fieldLeft | $fieldRight)";
85 if ( strcspn( $s,
"\0\"`'." ) !== strlen( $s ) ) {
87 "Identifier must not contain quote, dot or null characters: got '$s'"
91 return $quoteChar . $s . $quoteChar;
131 $fields = is_array( $fields ) ? $fields : [ $fields ];
132 $values = is_array( $values ) ? $values : [ $values ];
135 foreach ( $fields as $alias => $field ) {
136 if ( is_int( $alias ) ) {
139 $encValues[] = $field;
142 foreach ( $values as $value ) {
143 if ( is_int( $value ) || is_float( $value ) ) {
144 $encValues[] = $value;
145 } elseif ( is_string( $value ) ) {
146 $encValues[] = $this->quoter->addQuotes( $value );
147 } elseif ( $value ===
null ) {
154 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
158 if ( !in_array( $op, [
'>',
'>=',
'<',
'<=' ] ) ) {
159 throw new InvalidArgumentException(
"Comparison operator must be one of '>', '>=', '<', '<='" );
161 if ( count( $conds ) === 0 ) {
162 throw new InvalidArgumentException(
"Empty input" );
187 foreach ( array_reverse( $conds ) as $field => $value ) {
188 if ( is_int( $field ) ) {
189 throw new InvalidArgumentException(
190 'Non-associative array passed to buildComparison() (typo?)'
193 $encValue = $this->quoter->addQuotes( $value );
195 $sql =
"$field $op $encValue";
197 $op = rtrim( $op,
'=' );
199 $sql =
"$field $op $encValue OR ($field = $encValue AND ($sql))";
205 public function makeList( array $a, $mode = self::LIST_COMMA ) {
210 foreach ( $a as $field => $value ) {
214 if ( $mode == self::LIST_AND ) {
216 } elseif ( $mode == self::LIST_OR ) {
223 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
225 $list .=
"(" . $value->toSql( $this->quoter ) .
")";
226 } elseif ( is_array( $value ) ) {
227 throw new InvalidArgumentException( __METHOD__ .
": unexpected array value without key" );
229 throw new InvalidArgumentException( __METHOD__ .
": unexpected raw value without key" );
234 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
235 throw new InvalidArgumentException( __METHOD__ .
": unexpected key $field for IExpression value" );
237 throw new InvalidArgumentException( __METHOD__ .
": unexpected IExpression outside WHERE clause" );
239 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
242 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
245 $includeNull =
false;
246 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
248 unset( $value[$nullKey] );
250 if ( count( $value ) == 0 && !$includeNull ) {
251 throw new InvalidArgumentException(
252 __METHOD__ .
": empty input for field $field" );
253 } elseif ( count( $value ) == 0 ) {
255 $list .=
"$field IS NULL";
258 if ( $includeNull ) {
262 if ( count( $value ) == 1 ) {
265 $list .= $field .
" = " . $this->makeList( $value );
267 $list .= $field .
" IN (" . $this->makeList( $value ) .
") ";
270 if ( $includeNull ) {
271 $list .=
" OR $field IS NULL)";
274 } elseif ( is_array( $value ) ) {
275 throw new InvalidArgumentException( __METHOD__ .
": unexpected nested array" );
276 } elseif ( $value ===
null ) {
277 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
278 $list .=
"$field IS ";
279 } elseif ( $mode == self::LIST_SET ) {
280 $list .=
"$field = ";
281 } elseif ( $mode === self::LIST_COMMA && !is_numeric( $field ) ) {
283 __METHOD__ .
": array key {key} in list of values ignored",
284 [
'key' => $field,
'exception' =>
new RuntimeException() ]
286 } elseif ( $mode === self::LIST_NAMES && !is_numeric( $field ) ) {
288 __METHOD__ .
": array key {key} in list of fields ignored",
289 [
'key' => $field,
'exception' =>
new RuntimeException() ]
295 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
297 $list .=
"$field = ";
298 } elseif ( $mode === self::LIST_COMMA && !is_numeric( $field ) ) {
300 __METHOD__ .
": array key {key} in list of values ignored",
301 [
'key' => $field,
'exception' =>
new RuntimeException() ]
303 } elseif ( $mode === self::LIST_NAMES && !is_numeric( $field ) ) {
305 __METHOD__ .
": array key {key} in list of fields ignored",
306 [
'key' => $field,
'exception' =>
new RuntimeException() ]
309 $list .= $mode == self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
316 $this->logger->warning( ...$keyWarning );
324 foreach ( $data as $base => $sub ) {
325 if ( count( $sub ) ) {
326 $conds[] = $this->makeList(
327 [ $baseKey => $base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
334 throw new InvalidArgumentException(
"Data for $baseKey and $subKey must be non-empty" );
337 return $this->makeList( $conds, self::LIST_OR );
341 if ( count( $condsArray ) === 0 ) {
342 throw new InvalidArgumentException(
343 __METHOD__ .
": empty condition array" );
345 $condsByFieldSet = [];
346 foreach ( $condsArray as $conds ) {
347 if ( !count( $conds ) ) {
348 throw new InvalidArgumentException(
349 __METHOD__ .
": empty condition subarray" );
351 $fieldKey = implode(
',', array_keys( $conds ) );
352 $condsByFieldSet[$fieldKey][] = $conds;
355 foreach ( $condsByFieldSet as $conds ) {
356 if ( $result !==
'' ) {
359 $result .= $this->factorCondsWithCommonFields( $conds );
371 private function factorCondsWithCommonFields( $condsArray ) {
372 $first = $condsArray[array_key_first( $condsArray )];
373 if ( count( $first ) === 1 ) {
375 $field = array_key_first( $first );
377 foreach ( $condsArray as $conds ) {
378 $values[] = $conds[$field];
380 return $this->makeList( [ $field => $values ], self::LIST_AND );
383 $field1 = array_key_first( $first );
384 $nullExpressions = [];
385 $expressionsByField1 = [];
386 foreach ( $condsArray as $conds ) {
387 $value1 = $conds[$field1];
388 unset( $conds[$field1] );
389 if ( $value1 ===
null ) {
390 $nullExpressions[] = $conds;
392 $expressionsByField1[$value1][] = $conds;
398 foreach ( $expressionsByField1 as $value1 => $expressions ) {
399 if ( $result !==
'' ) {
403 $factored = $this->factorCondsWithCommonFields( $expressions );
404 $result .=
"($field1 = " . $this->quoter->addQuotes( $value1 ) .
407 if ( count( $nullExpressions ) ) {
408 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
409 if ( $result !==
'' ) {
413 $result .=
"($field1 IS NULL AND $factored)";
426 return 'CONCAT(' . implode(
',', $stringList ) .
')';
430 if ( !is_numeric( $limit ) ) {
432 "Invalid non-numeric limit passed to " . __METHOD__
438 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
449 [ $escapeChar,
'%',
'_' ],
450 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
456 if ( is_array( $param ) ) {
458 $param = array_shift( $params );
460 $likeValue =
new LikeValue( $param, ...$params );
462 return ' LIKE ' . $likeValue->toSql( $this->quoter );
481 $glue = $all ?
') UNION ALL (' :
') UNION (';
483 $sql =
'(' . implode( $glue, $sqls ) .
')';
484 if ( !$this->unionSupportsOrderAndLimit() ) {
487 $sql .= $this->makeOrderBy( $options );
488 $limit = $options[
'LIMIT'] ??
null;
489 $offset = $options[
'OFFSET'] ??
false;
490 if ( $limit !==
null ) {
491 $sql = $this->limitResult( $sql, $limit, $offset );
497 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
498 if ( is_array( $cond ) ) {
499 $cond = $this->makeList( $cond, self::LIST_AND );
502 $cond = $cond->toSql( $this->quoter );
505 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
509 return "REPLACE({$orig}, {$old}, {$new})";
513 $t =
new ConvertibleTimestamp( $ts );
515 return $t->getTimestamp( TS_MW );
519 if ( $ts ===
null ) {
522 return $this->timestamp( $ts );
531 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() )
532 ? $this->getInfinity()
533 : $this->timestamp( $expiry );
537 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() ) {
541 return ConvertibleTimestamp::convert( $format, $expiry );
548 $this->assertBuildSubstringParams( $startPosition, $length );
549 $functionBody =
"$input FROM $startPosition";
550 if ( $length !==
null ) {
551 $functionBody .=
" FOR $length";
553 return 'SUBSTRING(' . $functionBody .
')';
569 if ( $startPosition === 0 ) {
571 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
573 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
574 throw new InvalidArgumentException(
575 '$startPosition must be a positive integer'
578 if ( !( ( is_int( $length ) && $length >= 0 ) || $length ===
null ) ) {
579 throw new InvalidArgumentException(
580 '$length must be null or an integer greater than or equal to 0'
588 return "CAST( $field AS CHARACTER )";
592 return 'CAST( ' . $field .
' AS INTEGER )';
600 $this->tableAliases = $aliases;
607 return $this->tableAliases;
612 $this->currentDomain->getDatabase(),
613 $this->currentDomain->getSchema(),
619 $this->currentDomain = $currentDomain;
627 return $this->currentDomain;
631 $tables, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
633 if ( !is_array( $tables ) ) {
634 if ( $tables ===
'' || $tables ===
null || $tables ===
false ) {
636 } elseif ( is_string( $tables ) ) {
637 $tables = [ $tables ];
639 throw new DBLanguageError( __METHOD__ .
' called with incorrect table parameter' );
643 if ( is_array( $vars ) ) {
644 $fields = implode(
',', $this->fieldNamesWithAlias( $vars ) );
649 $options = (array)$options;
651 $useIndexByTable = $options[
'USE INDEX'] ?? [];
652 if ( !is_array( $useIndexByTable ) ) {
653 if ( count( $tables ) <= 1 ) {
654 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
656 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous USE INDEX ($fname)" );
657 ( $this->errorLogger )( $e );
661 $ignoreIndexByTable = $options[
'IGNORE INDEX'] ?? [];
662 if ( !is_array( $ignoreIndexByTable ) ) {
663 if ( count( $tables ) <= 1 ) {
664 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
666 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous IGNORE INDEX ($fname)" );
667 ( $this->errorLogger )( $e );
672 $this->selectOptionsIncludeLocking( $options ) &&
673 $this->selectFieldsOrOptionsAggregate( $vars, $options )
677 $this->logger->warning(
678 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
682 if ( count( $tables ) ) {
683 $from =
' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
693 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
695 if ( is_array( $conds ) ) {
696 $where = $this->makeList( $conds, self::LIST_AND );
698 $where = $conds->toSql( $this->quoter );
699 } elseif ( $conds ===
null || $conds ===
false ) {
701 $this->logger->warning(
705 .
' with incorrect parameters: $conds must be a string or an array',
706 [
'db_log_category' =>
'sql' ]
708 } elseif ( is_string( $conds ) ) {
711 throw new DBLanguageError( __METHOD__ .
' called with incorrect parameters' );
715 if ( $where ===
'' || $where ===
'*' ) {
716 $sql =
"SELECT $startOpts $fields $from $preLimitTail";
718 $sql =
"SELECT $startOpts $fields $from WHERE $where $preLimitTail";
721 if ( isset( $options[
'LIMIT'] ) ) {
722 $sql = $this->limitResult( $sql, $options[
'LIMIT'], $options[
'OFFSET'] ??
false );
724 $sql =
"$sql $postLimitTail";
726 if ( isset( $options[
'EXPLAIN'] ) ) {
727 $sql =
'EXPLAIN ' . $sql;
731 $fname === static::CALLER_UNKNOWN ||
732 str_starts_with( $fname,
'Wikimedia\\Rdbms\\' ) ||
733 $fname ===
'{closure}'
735 $exception =
new RuntimeException();
739 foreach ( $exception->getTrace() as $call ) {
740 if ( str_ends_with( $call[
'file'] ??
'',
'Test.php' ) ) {
743 } elseif ( str_starts_with( $call[
'class'] ??
'',
'Wikimedia\\Rdbms\\' ) ) {
745 } elseif ( str_ends_with( $call[
'class'] ??
'',
'SelectQueryBuilder' ) ) {
749 $caller = implode(
'::', array_filter( [ $call[
'class'] ??
null, $call[
'function'] ] ) );
754 if ( $fname ===
'{closure}' ) {
760 $warning =
"SQL query with incorrect caller (__METHOD__ used inside a closure: {caller}): {sql}";
762 $warning =
"SQL query did not specify the caller (guessed caller: {caller}): {sql}";
765 $this->logger->warning(
767 [
'sql' => $sql,
'caller' => $caller,
'exception' => $exception ]
778 private function selectOptionsIncludeLocking( $options ) {
779 $options = (array)$options;
780 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
781 if ( in_array( $lock, $options,
true ) ) {
794 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
795 foreach ( (array)$options as $key => $value ) {
796 if ( is_string( $key ) ) {
797 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
800 } elseif ( is_string( $value ) ) {
801 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
807 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
808 foreach ( (array)$fields as $field ) {
809 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
825 foreach ( $fields as $alias => $field ) {
826 if ( is_numeric( $alias ) ) {
829 $retval[] = $this->fieldNameWithAlias( $field, $alias );
844 if ( !$alias || (
string)$alias === (
string)$name ) {
847 return $name .
' AS ' . $this->addIdentifierQuotes( $alias );
869 $use_index = (array)$use_index;
870 $ignore_index = (array)$ignore_index;
871 $join_conds = (array)$join_conds;
873 foreach ( $tables as $alias => $table ) {
874 if ( !is_string( $alias ) ) {
879 if ( is_array( $table ) ) {
881 if ( count( $table ) > 1 ) {
883 $this->tableNamesWithIndexClauseOrJOIN(
884 $table, $use_index, $ignore_index, $join_conds ) .
')';
887 $innerTable = reset( $table );
888 $innerAlias = key( $table );
889 $joinedTable = $this->tableNameWithAlias(
891 is_string( $innerAlias ) ? $innerAlias : $innerTable
895 $joinedTable = $this->tableNameWithAlias( $table, $alias );
899 if ( isset( $join_conds[$alias] ) ) {
900 Assert::parameterType(
'array', $join_conds[$alias],
"join_conds[$alias]" );
901 [ $joinType, $conds ] = $join_conds[$alias];
902 $tableClause = $this->normalizeJoinType( $joinType );
903 $tableClause .=
' ' . $joinedTable;
904 if ( isset( $use_index[$alias] ) ) {
905 $use = $this->useIndexClause( implode(
',', (array)$use_index[$alias] ) );
907 $tableClause .=
' ' . $use;
910 if ( isset( $ignore_index[$alias] ) ) {
911 $ignore = $this->ignoreIndexClause(
912 implode(
',', (array)$ignore_index[$alias] ) );
913 if ( $ignore !=
'' ) {
914 $tableClause .=
' ' . $ignore;
917 $on = $this->makeList( (array)$conds, self::LIST_AND );
919 $tableClause .=
' ON (' . $on .
')';
922 $retJOIN[] = $tableClause;
923 } elseif ( isset( $use_index[$alias] ) ) {
925 $tableClause = $joinedTable;
926 $tableClause .=
' ' . $this->useIndexClause(
927 implode(
',', (array)$use_index[$alias] )
930 $ret[] = $tableClause;
931 } elseif ( isset( $ignore_index[$alias] ) ) {
933 $tableClause = $joinedTable;
934 $tableClause .=
' ' . $this->ignoreIndexClause(
935 implode(
',', (array)$ignore_index[$alias] )
938 $ret[] = $tableClause;
940 $tableClause = $joinedTable;
942 $ret[] = $tableClause;
947 $implicitJoins = implode(
',', $ret );
948 $explicitJoins = implode(
' ', $retJOIN );
951 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
963 switch ( strtoupper( $joinType ) ) {
971 case 'STRAIGHT_JOIN':
972 case 'STRAIGHT JOIN':
993 if ( is_string( $table ) ) {
994 $quotedTable = $this->tableName( $table );
995 } elseif ( $table instanceof
Subquery ) {
996 $quotedTable = (string)$table;
998 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
1001 if ( $alias ===
false ) {
1002 if ( $table instanceof
Subquery ) {
1003 throw new InvalidArgumentException(
"Subquery table missing alias" );
1005 $quotedTableWithAnyAlias = $quotedTable;
1007 $alias === $table &&
1009 str_contains( $alias,
'.' ) ||
1010 $this->tableName( $alias,
'raw' ) === $table
1013 $quotedTableWithAnyAlias = $quotedTable;
1015 $quotedTableWithAnyAlias = $quotedTable .
' ' . $this->addIdentifierQuotes( $alias );
1018 return $quotedTableWithAnyAlias;
1021 public function tableName(
string $name, $format =
'quoted' ) {
1022 $prefix = $this->currentDomain->getTablePrefix();
1027 str_contains( $name,
'.' ) &&
1028 !preg_match(
'/^information_schema\.[a-z_0-9]+$/', $name )
1030 ( $prefix !==
'' && str_starts_with( $name, $prefix ) )
1032 $this->logger->warning(
1033 __METHOD__ .
' called with qualified table ' . $name,
1034 [
'db_log_category' =>
'sql' ]
1039 $formattedComponents = [];
1040 foreach ( $this->qualifiedTableComponents( $name ) as $component ) {
1041 if ( $format ===
'quoted' ) {
1042 $formattedComponents[] = $this->addIdentifierQuotes( $component );
1044 $formattedComponents[] = $component;
1048 return implode(
'.', $formattedComponents );
1074 $identifiers = $this->extractTableNameComponents( $name );
1075 if ( count( $identifiers ) > 3 ) {
1076 throw new DBLanguageError(
"Too many components in table name '$name'" );
1079 if ( count( $identifiers ) == 1 && !$this->isQuotedIdentifier( $identifiers[0] ) ) {
1080 [ $table ] = $identifiers;
1081 if ( isset( $this->tableAliases[$table] ) ) {
1083 $database = $this->tableAliases[$table][
'dbname'];
1084 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1085 ? $this->tableAliases[$table][
'schema']
1086 : $this->relationSchemaQualifier();
1087 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1088 ? $this->tableAliases[$table][
'prefix']
1089 : $this->currentDomain->getTablePrefix();
1093 $schema = $this->relationSchemaQualifier();
1094 $prefix = $this->currentDomain->getTablePrefix();
1096 $qualifierIdentifiers = [ $database, $schema ];
1097 $tableIdentifier = $prefix . $table;
1099 $qualifierIdentifiers = array_slice( $identifiers, 0, -1 );
1100 $tableIdentifier = end( $identifiers );
1104 foreach ( $qualifierIdentifiers as $identifier ) {
1105 if ( $identifier !==
null && $identifier !==
'' ) {
1106 $components[] = $this->isQuotedIdentifier( $identifier )
1107 ? substr( $identifier, 1, -1 )
1111 $components[] = $this->isQuotedIdentifier( $tableIdentifier )
1112 ? substr( $tableIdentifier, 1, -1 )
1125 $quoteChar = $this->getIdentifierQuoteChar();
1127 foreach ( explode(
'.', $name ) as $component ) {
1128 if ( $this->isQuotedIdentifier( $component ) ) {
1129 $unquotedComponent = substr( $component, 1, -1 );
1131 $unquotedComponent = $component;
1133 if ( str_contains( $unquotedComponent, $quoteChar ) ) {
1135 'Table name component contains unexpected quote or dot character' );
1137 $components[] = $component;
1168 $components = $this->qualifiedTableComponents( $table );
1169 switch ( count( $components ) ) {
1171 return [ $this->currentDomain->getDatabase(), $components[0] ];
1183 return $this->currentDomain->getSchema();
1189 foreach ( $tables as $name ) {
1190 $retVal[] = $this->tableName( $name );
1206 $quoteChar = $this->getIdentifierQuoteChar();
1207 return strlen( $name ) > 1 && $name[0] === $quoteChar && $name[-1] === $quoteChar;
1249 $preLimitTail = $postLimitTail =
'';
1254 foreach ( $options as $key => $option ) {
1255 if ( is_numeric( $key ) ) {
1256 $noKeyOptions[$option] =
true;
1260 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1262 $preLimitTail .= $this->makeOrderBy( $options );
1264 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1265 $postLimitTail .=
' FOR UPDATE';
1268 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1269 $postLimitTail .=
' LOCK IN SHARE MODE';
1272 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1273 $startOpts .=
'DISTINCT';
1276 # Various MySQL extensions
1277 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1278 $startOpts .=
' /*! STRAIGHT_JOIN */';
1281 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1282 $startOpts .=
' SQL_BIG_RESULT';
1285 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1286 $startOpts .=
' SQL_BUFFER_RESULT';
1289 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1290 $startOpts .=
' SQL_SMALL_RESULT';
1293 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1294 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1297 return [ $startOpts, $preLimitTail, $postLimitTail ];
1310 if ( isset( $options[
'GROUP BY'] ) ) {
1311 $gb = is_array( $options[
'GROUP BY'] )
1312 ? implode(
',', $options[
'GROUP BY'] )
1313 : $options[
'GROUP BY'];
1314 $sql .=
' GROUP BY ' . $gb;
1316 if ( isset( $options[
'HAVING'] ) ) {
1317 if ( $options[
'HAVING'] instanceof
IExpression ) {
1318 $having = $options[
'HAVING']->toSql( $this->quoter );
1319 } elseif ( is_array( $options[
'HAVING'] ) ) {
1320 $having = $this->makeList( $options[
'HAVING'], self::LIST_AND );
1322 $having = $options[
'HAVING'];
1325 $sql .=
' HAVING ' . $having;
1340 if ( isset( $options[
'ORDER BY'] ) ) {
1341 $ob = is_array( $options[
'ORDER BY'] )
1342 ? implode(
',', $options[
'ORDER BY'] )
1343 : $options[
'ORDER BY'];
1345 return ' ORDER BY ' . $ob;
1352 $delim, $tables, $field, $conds =
'', $join_conds = []
1354 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) .
')';
1356 return '(' . $this->selectSQLText( $tables, $fld, $conds, static::CALLER_SUBQUERY, [], $join_conds ) .
')';
1360 $tables, $vars, $conds =
'', $fname = __METHOD__,
1361 $options = [], $join_conds = []
1364 $this->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds )
1369 $encTable = $this->tableName( $table );
1370 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1373 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1374 "INSERT INTO $encTable ($sqlColumns) VALUES '?'"
1390 public function makeInsertLists( array $rows, $aliasPrefix =
'', array $typeByColumn = [] ) {
1391 $firstRow = $rows[0];
1392 if ( !is_array( $firstRow ) || !$firstRow ) {
1396 $tupleColumns = array_keys( $firstRow );
1399 foreach ( $rows as $row ) {
1400 $rowColumns = array_keys( $row );
1402 if ( $rowColumns !== $tupleColumns ) {
1404 'All rows must specify the same columns in multi-row inserts. Found a row with (' .
1405 implode(
', ', $rowColumns ) .
') ' .
1406 'instead of expected (' . implode(
', ', $tupleColumns ) .
') as in the first row'
1410 $valueTuples[] =
'(' . $this->makeList( array_values( $row ), self::LIST_COMMA ) .
')';
1413 $magicAliasFields = [];
1414 foreach ( $tupleColumns as $column ) {
1415 $magicAliasFields[] = $aliasPrefix . $column;
1419 $this->makeList( $tupleColumns, self::LIST_NAMES ),
1420 implode(
',', $valueTuples ),
1421 $this->makeList( $magicAliasFields, self::LIST_NAMES )
1426 $encTable = $this->tableName( $table );
1427 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1428 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1431 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" ),
1432 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts" )
1441 return [
'INSERT IGNORE INTO',
'' ];
1450 array $insertOptions,
1451 array $selectOptions,
1454 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions(
'IGNORE', $insertOptions )
1455 ? $this->makeInsertNonConflictingVerbAndOptions()
1456 : [
'INSERT INTO',
'' ];
1457 $encDstTable = $this->tableName( $destTable );
1458 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
1459 $selectSql = $this->selectSQLText(
1461 array_values( $varMap ),
1468 return rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1478 foreach ( array_keys( $options, $option,
true ) as $k ) {
1479 if ( is_int( $k ) ) {
1497 } elseif ( !$uniqueKey ) {
1501 if ( count( $uniqueKey ) == 1 ) {
1503 $column = reset( $uniqueKey );
1504 $values = array_column( $rows, $column );
1505 if ( count( $values ) !== count( $rows ) ) {
1506 throw new DBLanguageError(
"Missing values for unique key ($column)" );
1509 return $this->makeList( [ $column => $values ], self::LIST_AND );
1512 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
1515 foreach ( $rows as $row ) {
1516 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1517 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1519 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
1522 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
1525 return count( $orConds ) > 1
1526 ? $this->makeList( $orConds, self::LIST_OR )
1532 throw new DBLanguageError( __METHOD__ .
' called with empty $conds' );
1535 $delTable = $this->tableName( $delTable );
1536 $joinTable = $this->tableName( $joinTable );
1537 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1538 if ( $conds !=
'*' ) {
1539 $sql .=
'WHERE ' . $this->makeList( $conds, self::LIST_AND );
1552 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1553 if ( !$isCondValid ) {
1554 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1557 $encTable = $this->tableName( $table );
1558 $sql =
"DELETE FROM $encTable";
1561 $cleanCondsSql =
'';
1562 if ( $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1563 $cleanCondsSql =
' WHERE ' . $this->scrubArray( $conds );
1564 if ( is_array( $conds ) ) {
1565 $conds = $this->makeList( $conds, self::LIST_AND );
1567 $condsSql .=
' WHERE ' . $conds;
1571 self::QUERY_CHANGE_ROWS,
1574 $sql . $cleanCondsSql
1582 private function scrubArray( $array,
int $listType = self::LIST_AND ): string {
1583 if ( is_array( $array ) ) {
1584 $scrubbedArray = [];
1585 foreach ( $array as $key => $value ) {
1587 $scrubbedArray[$key] = $value->toGeneralizedSql();
1589 $scrubbedArray[$key] =
'?';
1592 return $this->makeList( $scrubbedArray, $listType );
1598 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1599 if ( !$isCondValid ) {
1600 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1602 $encTable = $this->tableName( $table );
1603 $opts = $this->makeUpdateOptions( $options );
1604 $sql =
"UPDATE $opts $encTable";
1605 $condsSql =
" SET " . $this->makeList( $set, self::LIST_SET );
1606 $cleanCondsSql =
" SET " . $this->scrubArray( $set, self::LIST_SET );
1608 if ( $conds && $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1609 $cleanCondsSql .=
' WHERE ' . $this->scrubArray( $conds );
1610 if ( is_array( $conds ) ) {
1611 $conds = $this->makeList( $conds, self::LIST_AND );
1613 $condsSql .=
' WHERE ' . $conds;
1617 self::QUERY_CHANGE_ROWS,
1620 $sql . $cleanCondsSql
1631 $opts = $this->makeUpdateOptionsArray( $options );
1633 return implode(
' ', $opts );
1643 $options = $this->normalizeOptions( $options );
1647 if ( in_array(
'IGNORE', $options ) ) {
1660 if ( is_array( $options ) ) {
1662 } elseif ( is_string( $options ) ) {
1663 return ( $options ===
'' ) ? [] : [ $options ];
1665 throw new DBLanguageError( __METHOD__ .
': expected string or array' );
1673 return "DROP TABLE " . $this->tableName( $table ) .
" CASCADE";
1683 return QueryBuilderFromRawSql::buildQuery( $sql, 0 )->getVerb();
1704 'ROLLBACK TO SAVEPOINT',
1720 return "(SELECT __$column FROM __VALS)";
1724 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1728 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1732 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1740 $rows = $this->normalizeRowArray( $rows );
1745 $options = $this->normalizeOptions( $options );
1746 if ( $this->isFlagInOptions(
'IGNORE', $options ) ) {
1747 [ $sql, $cleanSql ] = $this->insertNonConflictingSqlText( $table, $rows );
1749 [ $sql, $cleanSql ] = $this->insertSqlText( $table, $rows );
1751 return new Query( $sql, self::QUERY_CHANGE_ROWS,
'INSERT', $table, $cleanSql );
1760 if ( !$rowOrRows ) {
1762 } elseif ( isset( $rowOrRows[0] ) ) {
1765 $rows = [ $rowOrRows ];
1768 foreach ( $rows as $row ) {
1769 if ( !is_array( $row ) ) {
1771 } elseif ( !$row ) {
1788 $rows = $this->normalizeRowArray( $rows );
1789 if ( !$uniqueKeys ) {
1790 throw new DBLanguageError(
'No unique key specified for upsert/replace' );
1792 $uniqueKey = $this->normalizeUpsertKeys( $uniqueKeys );
1793 $this->assertValidUpsertRowArray( $rows, $uniqueKey );
1805 if ( $conds ===
null || $conds ===
false ) {
1806 $this->logger->warning(
1810 .
' with incorrect parameters: $conds must be a string or an array',
1811 [
'db_log_category' =>
'sql' ]
1814 } elseif ( $conds ===
'' ) {
1818 return is_array( $conds ) ? $conds : [ $conds ];
1826 private function normalizeUpsertKeys( $uniqueKeys ) {
1827 if ( is_string( $uniqueKeys ) ) {
1828 return [ $uniqueKeys ];
1829 } elseif ( !is_array( $uniqueKeys ) ) {
1832 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1833 throw new DBLanguageError(
1834 "The unique key array should contain a single unique index" );
1837 $uniqueKey = $uniqueKeys[0];
1838 if ( is_string( $uniqueKey ) ) {
1841 $this->logger->warning( __METHOD__ .
1842 " called with deprecated parameter style: " .
1843 "the unique key array should be a string or array of string arrays",
1845 'exception' =>
new RuntimeException(),
1846 'db_log_category' =>
'sql',
1849 } elseif ( is_array( $uniqueKey ) ) {
1852 throw new DBLanguageError(
'Invalid unique key array entry' );
1863 foreach ( $rows as $row ) {
1864 foreach ( $uniqueKey as $column ) {
1865 if ( !isset( $row[$column] ) ) {
1867 "NULL/absent values for unique key (" . implode(
',', $uniqueKey ) .
")"
1886 throw new DBLanguageError(
"Update assignment list can't be empty for upsert" );
1891 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
1895 foreach ( $set as $k => $v ) {
1896 if ( is_string( $k ) ) {
1898 if ( in_array( $k, $uniqueKey,
true ) ) {
1899 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1900 $this->logger->warning(
1901 __METHOD__ .
" called with redundant assignment to column '$k'",
1903 'exception' =>
new RuntimeException(),
1904 'db_log_category' =>
'sql',
1909 "Cannot reassign column '$k' since it belongs to the provided unique key"
1913 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1915 if ( in_array( $m[1], $uniqueKey,
true ) ) {
1917 "Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
1929 if ( is_array( $var ) ) {
1932 } elseif ( count( $var ) == 1 ) {
1933 $column = $var[0] ?? reset( $var );
1945 $this->schemaVars = is_array( $vars ) ? $vars :
null;
1955 return $this->schemaVars ?? $this->getDefaultSchemaVars();
1991 $vars = $this->getSchemaVars();
1992 return preg_replace_callback(
1994 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
1995 \'\{\$ (\w+) }\' | # 3. addQuotes
1996 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
1997 /\*\$ (\w+) \*/ # 5. leave unencoded
1999 function ( $m ) use ( $vars ) {
2002 if ( isset( $m[1] ) && $m[1] !==
'' ) {
2003 if ( $m[1] ===
'i' ) {
2006 return $this->tableName( $m[2] );
2008 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
2009 return $this->quoter->addQuotes( $vars[$m[3]] );
2010 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
2011 return $this->addIdentifierQuotes( $vars[$m[4]] );
2012 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
2013 return $vars[$m[5]];
2023 throw new RuntimeException(
'locking must be implemented in subclasses' );
2027 throw new RuntimeException(
'locking must be implemented in subclasses' );
2031 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.
if(!defined('MW_SETUP_CALLBACK'))
Class to handle database/schema/prefix specifications for IDatabase.