22use InvalidArgumentException;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
27use Wikimedia\Assert\Assert;
37use Wikimedia\Timestamp\ConvertibleTimestamp;
70 $this->logger =
$logger ??
new NullLogger();
72 $this->errorLogger =
$errorLogger ??
static function ( Throwable $e ) {
73 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
81 public function bitAnd( $fieldLeft, $fieldRight ) {
82 return "($fieldLeft & $fieldRight)";
85 public function bitOr( $fieldLeft, $fieldRight ) {
86 return "($fieldLeft | $fieldRight)";
90 if ( strcspn( $s,
"\0\"`'." ) !== strlen( $s ) ) {
92 "Identifier must not contain quote, dot or null characters"
96 return $quoteChar . $s . $quoteChar;
136 $fields = is_array( $fields ) ? $fields : [ $fields ];
137 $values = is_array( $values ) ? $values : [ $values ];
140 foreach ( $fields as $alias => $field ) {
141 if ( is_int( $alias ) ) {
144 $encValues[] = $field;
147 foreach ( $values as $value ) {
148 if ( is_int( $value ) || is_float( $value ) ) {
149 $encValues[] = $value;
150 } elseif ( is_string( $value ) ) {
151 $encValues[] = $this->quoter->addQuotes( $value );
152 } elseif ( $value ===
null ) {
159 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
163 if ( !in_array( $op, [
'>',
'>=',
'<',
'<=' ] ) ) {
164 throw new InvalidArgumentException(
"Comparison operator must be one of '>', '>=', '<', '<='" );
166 if ( count( $conds ) === 0 ) {
167 throw new InvalidArgumentException(
"Empty input" );
192 foreach ( array_reverse( $conds ) as $field => $value ) {
193 if ( is_int( $field ) ) {
194 throw new InvalidArgumentException(
195 'Non-associative array passed to buildComparison() (typo?)'
198 $encValue = $this->quoter->addQuotes( $value );
200 $sql =
"$field $op $encValue";
202 $op = rtrim( $op,
'=' );
204 $sql =
"$field $op $encValue OR ($field = $encValue AND ($sql))";
210 public function makeList( array $a, $mode = self::LIST_COMMA ) {
214 foreach ( $a as $field => $value ) {
218 if ( $mode == self::LIST_AND ) {
220 } elseif ( $mode == self::LIST_OR ) {
227 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
229 $list .=
"(" . $value->toSql( $this->quoter ) .
")";
233 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
236 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
239 $includeNull =
false;
240 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
242 unset( $value[$nullKey] );
244 if ( count( $value ) == 0 && !$includeNull ) {
245 throw new InvalidArgumentException(
246 __METHOD__ .
": empty input for field $field" );
247 } elseif ( count( $value ) == 0 ) {
249 $list .=
"$field IS NULL";
252 if ( $includeNull ) {
256 if ( count( $value ) == 1 ) {
260 $value = array_values( $value )[0];
261 $list .= $field .
" = " . $this->quoter->addQuotes( $value );
263 $list .= $field .
" IN (" . $this->makeList( $value ) .
") ";
266 if ( $includeNull ) {
267 $list .=
" OR $field IS NULL)";
270 } elseif ( $value ===
null ) {
271 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
272 $list .=
"$field IS ";
273 } elseif ( $mode == self::LIST_SET ) {
274 $list .=
"$field = ";
279 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
281 $list .=
"$field = ";
283 $list .= $mode == self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
292 foreach ( $data as $base => $sub ) {
293 if ( count( $sub ) ) {
294 $conds[] = $this->makeList(
295 [ $baseKey => $base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
302 throw new InvalidArgumentException(
"Data for $baseKey and $subKey must be non-empty" );
305 return $this->makeList( $conds, self::LIST_OR );
309 if ( count( $condsArray ) === 0 ) {
310 throw new InvalidArgumentException(
311 __METHOD__ .
": empty condition array" );
313 $condsByFieldSet = [];
314 foreach ( $condsArray as $conds ) {
315 if ( !count( $conds ) ) {
316 throw new InvalidArgumentException(
317 __METHOD__ .
": empty condition subarray" );
319 $fieldKey = implode(
',', array_keys( $conds ) );
320 $condsByFieldSet[$fieldKey][] = $conds;
323 foreach ( $condsByFieldSet as $conds ) {
324 if ( $result !==
'' ) {
327 $result .= $this->factorCondsWithCommonFields( $conds );
339 private function factorCondsWithCommonFields( $condsArray ) {
340 $first = $condsArray[array_key_first( $condsArray )];
341 if ( count( $first ) === 1 ) {
343 $field = array_key_first( $first );
345 foreach ( $condsArray as $conds ) {
346 $values[] = $conds[$field];
348 return $this->makeList( [ $field => $values ], self::LIST_AND );
351 $field1 = array_key_first( $first );
352 $nullExpressions = [];
353 $expressionsByField1 = [];
354 foreach ( $condsArray as $conds ) {
355 $value1 = $conds[$field1];
356 unset( $conds[$field1] );
357 if ( $value1 ===
null ) {
358 $nullExpressions[] = $conds;
360 $expressionsByField1[$value1][] = $conds;
366 foreach ( $expressionsByField1 as $value1 => $expressions ) {
367 if ( $result !==
'' ) {
371 $factored = $this->factorCondsWithCommonFields( $expressions );
372 $result .=
"($field1 = " . $this->quoter->addQuotes( $value1 ) .
375 if ( count( $nullExpressions ) ) {
376 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
377 if ( $result !==
'' ) {
381 $result .=
"($field1 IS NULL AND $factored)";
395 return 'CONCAT(' . implode(
',', $stringList ) .
')';
399 if ( !is_numeric( $limit ) ) {
401 "Invalid non-numeric limit passed to " . __METHOD__
407 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
419 [ $escapeChar,
'%',
'_' ],
420 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
426 if ( is_array( $param ) ) {
434 return ' LIKE ' . $likeValue->toSql( $this->quoter );
454 $glue = $all ?
') UNION ALL (' :
') UNION (';
456 $sql =
'(' . implode( $glue, $sqls ) .
')';
457 if ( !$this->unionSupportsOrderAndLimit() ) {
460 $sql .= $this->makeOrderBy( $options );
461 $limit = $options[
'LIMIT'] ??
null;
462 $offset = $options[
'OFFSET'] ??
false;
463 if ( $limit !==
null ) {
464 $sql = $this->limitResult( $sql, $limit, $offset );
470 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
471 if ( is_array( $cond ) ) {
472 $cond = $this->makeList( $cond, self::LIST_AND );
475 $cond = $cond->toSql( $this->quoter );
478 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
482 return "REPLACE({$orig}, {$old}, {$new})";
486 $t =
new ConvertibleTimestamp( $ts );
488 return $t->getTimestamp( TS_MW );
492 if ( $ts ===
null ) {
495 return $this->timestamp( $ts );
504 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() )
505 ? $this->getInfinity()
506 : $this->timestamp( $expiry );
510 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() ) {
514 return ConvertibleTimestamp::convert( $format, $expiry );
522 $this->assertBuildSubstringParams( $startPosition, $length );
523 $functionBody =
"$input FROM $startPosition";
524 if ( $length !==
null ) {
525 $functionBody .=
" FOR $length";
527 return 'SUBSTRING(' . $functionBody .
')';
543 if ( $startPosition === 0 ) {
545 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
547 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
548 throw new InvalidArgumentException(
549 '$startPosition must be a positive integer'
552 if ( !( ( is_int( $length ) && $length >= 0 ) || $length ===
null ) ) {
553 throw new InvalidArgumentException(
554 '$length must be null or an integer greater than or equal to 0'
562 return "CAST( $field AS CHARACTER )";
566 return 'CAST( ' . $field .
' AS INTEGER )';
582 return $this->indexAliases[$index] ?? $index;
586 $this->tableAliases = $aliases;
590 $this->indexAliases = $aliases;
597 return $this->tableAliases;
602 $this->currentDomain->getDatabase(),
603 $this->currentDomain->getSchema(),
609 $this->currentDomain = $currentDomain;
613 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
615 if ( is_array( $table ) ) {
617 } elseif ( $table ===
'' || $table ===
null || $table ===
false ) {
619 } elseif ( is_string( $table ) ) {
620 $tables = [ $table ];
622 throw new DBLanguageError( __METHOD__ .
' called with incorrect table parameter' );
625 if ( is_array( $vars ) ) {
626 $fields = implode(
',', $this->fieldNamesWithAlias( $vars ) );
631 $options = (array)$options;
633 $useIndexByTable = $options[
'USE INDEX'] ?? [];
634 if ( !is_array( $useIndexByTable ) ) {
635 if ( count( $tables ) <= 1 ) {
636 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
638 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous USE INDEX ($fname)" );
639 ( $this->errorLogger )( $e );
643 $ignoreIndexByTable = $options[
'IGNORE INDEX'] ?? [];
644 if ( !is_array( $ignoreIndexByTable ) ) {
645 if ( count( $tables ) <= 1 ) {
646 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
648 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous IGNORE INDEX ($fname)" );
649 ( $this->errorLogger )( $e );
654 $this->selectOptionsIncludeLocking( $options ) &&
655 $this->selectFieldsOrOptionsAggregate( $vars, $options )
659 $this->logger->warning(
660 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
664 if ( count( $tables ) ) {
665 $from =
' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
675 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
677 if ( is_array( $conds ) ) {
678 $where = $this->makeList( $conds, self::LIST_AND );
680 $where = $conds->toSql( $this->quoter );
681 } elseif ( $conds ===
null || $conds ===
false ) {
683 $this->logger->warning(
687 .
' with incorrect parameters: $conds must be a string or an array',
688 [
'db_log_category' =>
'sql' ]
690 } elseif ( is_string( $conds ) ) {
693 throw new DBLanguageError( __METHOD__ .
' called with incorrect parameters' );
697 if ( $where ===
'' || $where ===
'*' ) {
698 $sql =
"SELECT $startOpts $fields $from $preLimitTail";
700 $sql =
"SELECT $startOpts $fields $from WHERE $where $preLimitTail";
703 if ( isset( $options[
'LIMIT'] ) ) {
704 $sql = $this->limitResult( $sql, $options[
'LIMIT'], $options[
'OFFSET'] ??
false );
706 $sql =
"$sql $postLimitTail";
708 if ( isset( $options[
'EXPLAIN'] ) ) {
709 $sql =
'EXPLAIN ' . $sql;
719 private function selectOptionsIncludeLocking( $options ) {
720 $options = (array)$options;
721 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
722 if ( in_array( $lock, $options,
true ) ) {
735 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
736 foreach ( (array)$options as $key => $value ) {
737 if ( is_string( $key ) ) {
738 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
741 } elseif ( is_string( $value ) ) {
742 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
748 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
749 foreach ( (array)$fields as $field ) {
750 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
766 foreach ( $fields as $alias => $field ) {
767 if ( is_numeric( $alias ) ) {
770 $retval[] = $this->fieldNameWithAlias( $field, $alias );
786 if ( !$alias || (
string)$alias === (
string)$name ) {
789 return $name .
' AS ' . $this->addIdentifierQuotes( $alias );
811 $use_index = (array)$use_index;
812 $ignore_index = (array)$ignore_index;
813 $join_conds = (array)$join_conds;
815 foreach ( $tables as $alias => $table ) {
816 if ( !is_string( $alias ) ) {
821 if ( is_array( $table ) ) {
823 if ( count( $table ) > 1 ) {
825 $this->tableNamesWithIndexClauseOrJOIN(
826 $table, $use_index, $ignore_index, $join_conds ) .
')';
829 $innerTable = reset( $table );
830 $innerAlias = key( $table );
831 $joinedTable = $this->tableNameWithAlias(
833 is_string( $innerAlias ) ? $innerAlias : $innerTable
837 $joinedTable = $this->tableNameWithAlias( $table, $alias );
841 if ( isset( $join_conds[$alias] ) ) {
842 Assert::parameterType(
'array', $join_conds[$alias],
"join_conds[$alias]" );
843 [ $joinType, $conds ] = $join_conds[$alias];
844 $tableClause = $this->normalizeJoinType( $joinType );
845 $tableClause .=
' ' . $joinedTable;
846 if ( isset( $use_index[$alias] ) ) {
847 $use = $this->useIndexClause( implode(
',', (array)$use_index[$alias] ) );
849 $tableClause .=
' ' . $use;
852 if ( isset( $ignore_index[$alias] ) ) {
853 $ignore = $this->ignoreIndexClause(
854 implode(
',', (array)$ignore_index[$alias] ) );
855 if ( $ignore !=
'' ) {
856 $tableClause .=
' ' . $ignore;
859 $on = $this->makeList( (array)$conds, self::LIST_AND );
861 $tableClause .=
' ON (' . $on .
')';
864 $retJOIN[] = $tableClause;
865 } elseif ( isset( $use_index[$alias] ) ) {
867 $tableClause = $joinedTable;
868 $tableClause .=
' ' . $this->useIndexClause(
869 implode(
',', (array)$use_index[$alias] )
872 $ret[] = $tableClause;
873 } elseif ( isset( $ignore_index[$alias] ) ) {
875 $tableClause = $joinedTable;
876 $tableClause .=
' ' . $this->ignoreIndexClause(
877 implode(
',', (array)$ignore_index[$alias] )
880 $ret[] = $tableClause;
882 $tableClause = $joinedTable;
884 $ret[] = $tableClause;
889 $implicitJoins = implode(
',', $ret );
890 $explicitJoins = implode(
' ', $retJOIN );
893 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
905 switch ( strtoupper( $joinType ) ) {
913 case 'STRAIGHT_JOIN':
914 case 'STRAIGHT JOIN':
935 if ( is_string( $table ) ) {
936 $quotedTable = $this->tableName( $table );
937 } elseif ( $table instanceof
Subquery ) {
938 $quotedTable = (string)$table;
940 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
943 if ( $alias ===
false || $alias === $table ) {
945 throw new InvalidArgumentException(
"Subquery table missing alias" );
950 return $quotedTable .
' ' . $this->addIdentifierQuotes( $alias );
954 public function tableName(
string $name, $format =
'quoted' ) {
956 $formattedComponents = [];
957 foreach ( $this->qualifiedTableComponents( $name ) as $component ) {
958 if ( $format ===
'quoted' ) {
959 $formattedComponents[] = $this->addIdentifierQuotes( $component );
961 $formattedComponents[] = $component;
965 return implode(
'.', $formattedComponents );
991 $identifiers = $this->extractTableNameComponents( $name );
992 if ( count( $identifiers ) > 3 ) {
993 throw new DBLanguageError(
"Too many components in table name '$name'" );
996 if ( count( $identifiers ) == 1 && !$this->isQuotedIdentifier( $identifiers[0] ) ) {
997 [ $table ] = $identifiers;
998 if ( isset( $this->tableAliases[$table] ) ) {
1000 $database = $this->tableAliases[$table][
'dbname'];
1001 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1002 ? $this->tableAliases[$table][
'schema']
1003 : $this->relationSchemaQualifier();
1004 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1005 ? $this->tableAliases[$table][
'prefix']
1006 : $this->currentDomain->getTablePrefix();
1010 $schema = $this->relationSchemaQualifier();
1011 $prefix = $this->currentDomain->getTablePrefix();
1013 $qualifierIdentifiers = [ $database, $schema ];
1014 $tableIdentifier = $prefix . $table;
1016 $qualifierIdentifiers = array_slice( $identifiers, 0, -1 );
1017 $tableIdentifier = end( $identifiers );
1021 foreach ( $qualifierIdentifiers as $identifier ) {
1022 if ( $identifier !==
null && $identifier !==
'' ) {
1023 $components[] = $this->isQuotedIdentifier( $identifier )
1024 ? substr( $identifier, 1, -1 )
1028 $components[] = $this->isQuotedIdentifier( $tableIdentifier )
1029 ? substr( $tableIdentifier, 1, -1 )
1042 $quoteChar = $this->getIdentifierQuoteChar();
1044 foreach ( explode(
'.', $name ) as $component ) {
1045 if ( $this->isQuotedIdentifier( $component ) ) {
1046 $unquotedComponent = substr( $component, 1, -1 );
1048 $unquotedComponent = $component;
1050 if ( str_contains( $unquotedComponent, $quoteChar ) ) {
1052 'Table name component contains unexpected quote or dot character' );
1054 $components[] = $component;
1064 return $this->currentDomain->getSchema();
1070 foreach ( $tables as $name ) {
1071 $retVal[$name] = $this->tableName( $name );
1080 foreach ( $tables as $name ) {
1081 $retVal[] = $this->tableName( $name );
1097 $quoteChar = $this->getIdentifierQuoteChar();
1098 return strlen( $name ) > 1 && $name[0] === $quoteChar && $name[-1] === $quoteChar;
1142 $preLimitTail = $postLimitTail =
'';
1147 foreach ( $options as $key => $option ) {
1148 if ( is_numeric( $key ) ) {
1149 $noKeyOptions[$option] =
true;
1153 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1155 $preLimitTail .= $this->makeOrderBy( $options );
1157 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1158 $postLimitTail .=
' FOR UPDATE';
1161 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1162 $postLimitTail .=
' LOCK IN SHARE MODE';
1165 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1166 $startOpts .=
'DISTINCT';
1169 # Various MySQL extensions
1170 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1171 $startOpts .=
' /*! STRAIGHT_JOIN */';
1174 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1175 $startOpts .=
' SQL_BIG_RESULT';
1178 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1179 $startOpts .=
' SQL_BUFFER_RESULT';
1182 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1183 $startOpts .=
' SQL_SMALL_RESULT';
1186 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1187 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1190 return [ $startOpts, $preLimitTail, $postLimitTail ];
1203 if ( isset( $options[
'GROUP BY'] ) ) {
1204 $gb = is_array( $options[
'GROUP BY'] )
1205 ? implode(
',', $options[
'GROUP BY'] )
1206 : $options[
'GROUP BY'];
1207 $sql .=
' GROUP BY ' . $gb;
1209 if ( isset( $options[
'HAVING'] ) ) {
1210 $having = is_array( $options[
'HAVING'] )
1211 ? $this->makeList( $options[
'HAVING'], self::LIST_AND )
1212 : $options[
'HAVING'];
1213 $sql .=
' HAVING ' . $having;
1228 if ( isset( $options[
'ORDER BY'] ) ) {
1229 $ob = is_array( $options[
'ORDER BY'] )
1230 ? implode(
',', $options[
'ORDER BY'] )
1231 : $options[
'ORDER BY'];
1233 return ' ORDER BY ' . $ob;
1240 $delim, $table, $field, $conds =
'', $join_conds = []
1242 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) .
')';
1244 return '(' . $this->selectSQLText( $table, $fld, $conds, __METHOD__, [], $join_conds ) .
')';
1248 $table, $vars, $conds =
'', $fname = __METHOD__,
1249 $options = [], $join_conds = []
1252 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
1257 $encTable = $this->tableName( $table );
1258 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1261 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1262 "INSERT INTO $encTable ($sqlColumns) VALUES '?'"
1278 public function makeInsertLists( array $rows, $aliasPrefix =
'', array $typeByColumn = [] ) {
1279 $firstRow = $rows[0];
1280 if ( !is_array( $firstRow ) || !$firstRow ) {
1284 $tupleColumns = array_keys( $firstRow );
1287 foreach ( $rows as $row ) {
1288 $rowColumns = array_keys( $row );
1290 if ( $rowColumns !== $tupleColumns ) {
1292 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
1293 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
1297 $valueTuples[] =
'(' . $this->makeList( $row, self::LIST_COMMA ) .
')';
1300 $magicAliasFields = [];
1301 foreach ( $tupleColumns as $column ) {
1302 $magicAliasFields[] = $aliasPrefix . $column;
1306 $this->makeList( $tupleColumns, self::LIST_NAMES ),
1307 implode(
',', $valueTuples ),
1308 $this->makeList( $magicAliasFields, self::LIST_NAMES )
1313 $encTable = $this->tableName( $table );
1314 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1315 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1318 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" ),
1319 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts" )
1329 return [
'INSERT IGNORE INTO',
'' ];
1338 array $insertOptions,
1339 array $selectOptions,
1342 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions(
'IGNORE', $insertOptions )
1343 ? $this->makeInsertNonConflictingVerbAndOptions()
1344 : [
'INSERT INTO',
'' ];
1345 $encDstTable = $this->tableName( $destTable );
1346 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
1347 $selectSql = $this->selectSQLText(
1349 array_values( $varMap ),
1356 return rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1366 foreach ( array_keys( $options, $option,
true ) as $k ) {
1367 if ( is_int( $k ) ) {
1385 } elseif ( !$uniqueKey ) {
1389 if ( count( $uniqueKey ) == 1 ) {
1391 $column = reset( $uniqueKey );
1392 $values = array_column( $rows, $column );
1393 if ( count( $values ) !== count( $rows ) ) {
1394 throw new DBLanguageError(
"Missing values for unique key ($column)" );
1397 return $this->makeList( [ $column => $values ], self::LIST_AND );
1400 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
1403 foreach ( $rows as $row ) {
1404 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1405 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1407 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
1410 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
1413 return count( $orConds ) > 1
1414 ? $this->makeList( $orConds, self::LIST_OR )
1420 throw new DBLanguageError( __METHOD__ .
' called with empty $conds' );
1423 $delTable = $this->tableName( $delTable );
1424 $joinTable = $this->tableName( $joinTable );
1425 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1426 if ( $conds !=
'*' ) {
1427 $sql .=
'WHERE ' . $this->makeList( $conds, self::LIST_AND );
1440 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1441 if ( !$isCondValid ) {
1442 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1445 $encTable = $this->tableName( $table );
1446 $sql =
"DELETE FROM $encTable";
1449 $cleanCondsSql =
'';
1450 if ( $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1451 $cleanCondsSql =
' WHERE ' . $this->scrubArray( $conds );
1452 if ( is_array( $conds ) ) {
1453 $conds = $this->makeList( $conds, self::LIST_AND );
1455 $condsSql .=
' WHERE ' . $conds;
1459 self::QUERY_CHANGE_ROWS,
1462 $sql . $cleanCondsSql
1466 private function scrubArray( $array, $listType = self::LIST_AND ) {
1467 if ( is_array( $array ) ) {
1468 $scrubbedArray = [];
1469 foreach ( $array as $key => $value ) {
1471 $scrubbedArray[$key] = $value->toGeneralizedSql();
1473 $scrubbedArray[$key] =
'?';
1476 return $this->makeList( $scrubbedArray, $listType );
1482 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1483 if ( !$isCondValid ) {
1484 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1486 $encTable = $this->tableName( $table );
1487 $opts = $this->makeUpdateOptions( $options );
1488 $sql =
"UPDATE $opts $encTable";
1489 $condsSql =
" SET " . $this->makeList( $set, self::LIST_SET );
1490 $cleanCondsSql =
" SET " . $this->scrubArray( $set, self::LIST_SET );
1492 if ( $conds && $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1493 $cleanCondsSql .=
' WHERE ' . $this->scrubArray( $conds );
1494 if ( is_array( $conds ) ) {
1495 $conds = $this->makeList( $conds, self::LIST_AND );
1497 $condsSql .=
' WHERE ' . $conds;
1501 self::QUERY_CHANGE_ROWS,
1504 $sql . $cleanCondsSql
1515 $opts = $this->makeUpdateOptionsArray( $options );
1517 return implode(
' ', $opts );
1528 $options = $this->normalizeOptions( $options );
1532 if ( in_array(
'IGNORE', $options ) ) {
1545 if ( is_array( $options ) ) {
1547 } elseif ( is_string( $options ) ) {
1548 return ( $options ===
'' ) ? [] : [ $options ];
1550 throw new DBLanguageError( __METHOD__ .
': expected string or array' );
1558 return "DROP TABLE " . $this->tableName( $table ) .
" CASCADE";
1589 'ROLLBACK TO SAVEPOINT',
1605 return "(SELECT __$column FROM __VALS)";
1609 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1613 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1617 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1625 $rows = $this->normalizeRowArray( $rows );
1630 $options = $this->normalizeOptions( $options );
1631 if ( $this->isFlagInOptions(
'IGNORE', $options ) ) {
1632 [ $sql, $cleanSql ] = $this->insertNonConflictingSqlText( $table, $rows );
1634 [ $sql, $cleanSql ] = $this->insertSqlText( $table, $rows );
1636 return new Query( $sql, self::QUERY_CHANGE_ROWS,
'INSERT', $table, $cleanSql );
1645 if ( !$rowOrRows ) {
1647 } elseif ( isset( $rowOrRows[0] ) ) {
1650 $rows = [ $rowOrRows ];
1653 foreach ( $rows as $row ) {
1654 if ( !is_array( $row ) ) {
1656 } elseif ( !$row ) {
1673 $rows = $this->normalizeRowArray( $rows );
1674 if ( !$uniqueKeys ) {
1675 throw new DBLanguageError(
'No unique key specified for upsert/replace' );
1677 $uniqueKey = $this->normalizeUpsertKeys( $uniqueKeys );
1678 $this->assertValidUpsertRowArray( $rows, $uniqueKey );
1690 if ( $conds ===
null || $conds ===
false ) {
1691 $this->logger->warning(
1695 .
' with incorrect parameters: $conds must be a string or an array',
1696 [
'db_log_category' =>
'sql' ]
1699 } elseif ( $conds ===
'' ) {
1703 return is_array( $conds ) ? $conds : [ $conds ];
1711 private function normalizeUpsertKeys( $uniqueKeys ) {
1712 if ( is_string( $uniqueKeys ) ) {
1713 return [ $uniqueKeys ];
1714 } elseif ( !is_array( $uniqueKeys ) ) {
1717 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1718 throw new DBLanguageError(
1719 "The unique key array should contain a single unique index" );
1722 $uniqueKey = $uniqueKeys[0];
1723 if ( is_string( $uniqueKey ) ) {
1726 $this->logger->warning( __METHOD__ .
1727 " called with deprecated parameter style: " .
1728 "the unique key array should be a string or array of string arrays",
1730 'exception' =>
new RuntimeException(),
1731 'db_log_category' =>
'sql',
1734 } elseif ( is_array( $uniqueKey ) ) {
1737 throw new DBLanguageError(
'Invalid unique key array entry' );
1748 foreach ( $rows as $row ) {
1749 foreach ( $uniqueKey as $column ) {
1750 if ( !isset( $row[$column] ) ) {
1752 "NULL/absent values for unique key (" . implode(
',', $uniqueKey ) .
")"
1771 throw new DBLanguageError(
"Update assignment list can't be empty for upsert" );
1776 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
1780 foreach ( $set as $k => $v ) {
1781 if ( is_string( $k ) ) {
1783 if ( in_array( $k, $uniqueKey,
true ) ) {
1784 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1785 $this->logger->warning(
1786 __METHOD__ .
" called with redundant assignment to column '$k'",
1788 'exception' =>
new RuntimeException(),
1789 'db_log_category' =>
'sql',
1794 "Cannot reassign column '$k' since it belongs to the provided unique key"
1798 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1800 if ( in_array( $m[1], $uniqueKey,
true ) ) {
1802 "Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
1814 if ( is_array( $var ) ) {
1817 } elseif ( count( $var ) == 1 ) {
1818 $column = $var[0] ?? reset( $var );
1830 $this->schemaVars = is_array( $vars ) ? $vars :
null;
1840 return $this->schemaVars ?? $this->getDefaultSchemaVars();
1877 $vars = $this->getSchemaVars();
1878 return preg_replace_callback(
1880 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
1881 \'\{\$ (\w+) }\' | # 3. addQuotes
1882 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
1883 /\*\$ (\w+) \*/ # 5. leave unencoded
1885 function ( $m ) use ( $vars ) {
1888 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1889 if ( $m[1] ===
'i' ) {
1890 return $this->indexName( $m[2] );
1892 return $this->tableName( $m[2] );
1894 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
1895 return $this->quoter->addQuotes( $vars[$m[3]] );
1896 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
1897 return $this->addIdentifierQuotes( $vars[$m[4]] );
1898 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
1899 return $vars[$m[5]];
1909 throw new RuntimeException(
'locking must be implemented in subclasses' );
1913 throw new RuntimeException(
'locking must be implemented in subclasses' );
1917 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.
array $params
The job parameters.
if(!defined('MW_SETUP_CALLBACK'))
Class to handle database/schema/prefix specifications for IDatabase.