22use InvalidArgumentException;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
27use Wikimedia\Assert\Assert;
33use Wikimedia\Timestamp\ConvertibleTimestamp;
66 $this->logger =
$logger ??
new NullLogger();
68 $this->errorLogger =
$errorLogger ??
static function ( Throwable $e ) {
69 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
85 public function bitAnd( $fieldLeft, $fieldRight ) {
86 return "($fieldLeft & $fieldRight)";
93 public function bitOr( $fieldLeft, $fieldRight ) {
94 return "($fieldLeft | $fieldRight)";
102 return '"' . str_replace(
'"',
'""', $s ) .
'"';
134 $fields = is_array( $fields ) ? $fields : [ $fields ];
135 $values = is_array( $values ) ? $values : [ $values ];
138 foreach ( $fields as $alias => $field ) {
139 if ( is_int( $alias ) ) {
142 $encValues[] = $field;
145 foreach ( $values as $value ) {
146 if ( is_int( $value ) || is_float( $value ) ) {
147 $encValues[] = $value;
148 } elseif ( is_string( $value ) ) {
149 $encValues[] = $this->quoter->addQuotes( $value );
150 } elseif ( $value ===
null ) {
157 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
161 if ( !in_array( $op, [
'>',
'>=',
'<',
'<=' ] ) ) {
162 throw new InvalidArgumentException(
"Comparison operator must be one of '>', '>=', '<', '<='" );
164 if ( count( $conds ) === 0 ) {
165 throw new InvalidArgumentException(
"Empty input" );
190 foreach ( array_reverse( $conds ) as $field => $value ) {
191 $encValue = $this->quoter->addQuotes( $value );
193 $sql =
"$field $op $encValue";
195 $op = rtrim( $op,
'=' );
197 $sql =
"$field $op $encValue OR ($field = $encValue AND ($sql))";
203 public function makeList( array $a, $mode = self::LIST_COMMA ) {
207 foreach ( $a as $field => $value ) {
211 if ( $mode == self::LIST_AND ) {
213 } elseif ( $mode == self::LIST_OR ) {
220 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
222 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
225 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
228 $includeNull =
false;
229 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
231 unset( $value[$nullKey] );
233 if ( count( $value ) == 0 && !$includeNull ) {
234 throw new InvalidArgumentException(
235 __METHOD__ .
": empty input for field $field" );
236 } elseif ( count( $value ) == 0 ) {
238 $list .=
"$field IS NULL";
241 if ( $includeNull ) {
245 if ( count( $value ) == 1 ) {
249 $value = array_values( $value )[0];
250 $list .= $field .
" = " . $this->quoter->addQuotes( $value );
252 $list .= $field .
" IN (" . $this->makeList( $value ) .
") ";
255 if ( $includeNull ) {
256 $list .=
" OR $field IS NULL)";
259 } elseif ( $value ===
null ) {
260 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
261 $list .=
"$field IS ";
262 } elseif ( $mode == self::LIST_SET ) {
263 $list .=
"$field = ";
268 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
270 $list .=
"$field = ";
272 $list .= $mode == self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
282 foreach ( $data as
$base => $sub ) {
283 if ( count( $sub ) ) {
284 $conds[] = $this->makeList(
285 [ $baseKey =>
$base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
292 return $this->makeList( $conds, self::LIST_OR );
300 if ( count( $condsArray ) === 0 ) {
301 throw new InvalidArgumentException(
302 __METHOD__ .
": empty condition array" );
304 $condsByFieldSet = [];
305 foreach ( $condsArray as $conds ) {
306 if ( !count( $conds ) ) {
307 throw new InvalidArgumentException(
308 __METHOD__ .
": empty condition subarray" );
310 $fieldKey = implode(
',', array_keys( $conds ) );
311 $condsByFieldSet[$fieldKey][] = $conds;
314 foreach ( $condsByFieldSet as $conds ) {
315 if ( $result !==
'' ) {
318 $result .= $this->factorCondsWithCommonFields( $conds );
330 private function factorCondsWithCommonFields( $condsArray ) {
331 $first = $condsArray[array_key_first( $condsArray )];
332 if ( count( $first ) === 1 ) {
334 $field = array_key_first( $first );
336 foreach ( $condsArray as $conds ) {
337 $values[] = $conds[$field];
339 return $this->makeList( [ $field => $values ], self::LIST_AND );
342 $field1 = array_key_first( $first );
343 $nullExpressions = [];
344 $expressionsByField1 = [];
345 foreach ( $condsArray as $conds ) {
346 $value1 = $conds[$field1];
347 unset( $conds[$field1] );
348 if ( $value1 ===
null ) {
349 $nullExpressions[] = $conds;
351 $expressionsByField1[$value1][] = $conds;
357 foreach ( $expressionsByField1 as $value1 => $expressions ) {
358 if ( $result !==
'' ) {
362 $factored = $this->factorCondsWithCommonFields( $expressions );
363 $result .=
"($field1 = " . $this->quoter->addQuotes( $value1 ) .
366 if ( count( $nullExpressions ) ) {
367 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
368 if ( $result !==
'' ) {
372 $result .=
"($field1 IS NULL AND $factored)";
386 return 'CONCAT(' . implode(
',', $stringList ) .
')';
394 if ( !is_numeric( $limit ) ) {
396 "Invalid non-numeric limit passed to " . __METHOD__
402 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
414 [ $escapeChar,
'%',
'_' ],
415 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
425 if ( is_array( $param ) ) {
428 $params = func_get_args();
439 foreach ( $params as $value ) {
441 $s .= $value->toString();
443 $s .= $this->escapeLikeInternal( $value, $escapeChar );
448 $this->quoter->addQuotes( $s ) .
' ESCAPE ' . $this->quoter->addQuotes( $escapeChar ) .
' ';
472 $glue = $all ?
') UNION ALL (' :
') UNION (';
474 return '(' . implode( $glue, $sqls ) .
')';
481 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
482 if ( is_array( $cond ) ) {
483 $cond = $this->makeList( $cond, self::LIST_AND );
486 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
494 return "REPLACE({$orig}, {$old}, {$new})";
502 $t =
new ConvertibleTimestamp( $ts );
504 return $t->getTimestamp( TS_MW );
508 if ( $ts ===
null ) {
511 return $this->timestamp( $ts );
524 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() )
525 ? $this->getInfinity()
526 : $this->timestamp( $expiry );
530 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() ) {
534 return ConvertibleTimestamp::convert( $format, $expiry );
542 $this->assertBuildSubstringParams( $startPosition, $length );
543 $functionBody =
"$input FROM $startPosition";
544 if ( $length !==
null ) {
545 $functionBody .=
" FOR $length";
547 return 'SUBSTRING(' . $functionBody .
')';
563 if ( $startPosition === 0 ) {
565 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
567 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
568 throw new InvalidArgumentException(
569 '$startPosition must be a positive integer'
572 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
573 throw new InvalidArgumentException(
574 '$length must be null or an integer greater than or equal to 0'
586 return "CAST( $field AS CHARACTER )";
594 return 'CAST( ' . $field .
' AS INTEGER )';
614 return $this->indexAliases[$index] ?? $index;
622 $this->tableAliases = $aliases;
630 $this->indexAliases = $aliases;
637 return $this->tableAliases;
642 $this->currentDomain->getDatabase(),
643 $this->currentDomain->getSchema(),
649 $this->currentDomain = $currentDomain;
657 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
659 if ( is_array( $table ) ) {
661 } elseif ( $table ===
'' || $table ===
null || $table ===
false ) {
663 } elseif ( is_string( $table ) ) {
664 $tables = [ $table ];
666 throw new DBLanguageError( __METHOD__ .
' called with incorrect table parameter' );
669 if ( is_array( $vars ) ) {
670 $fields = implode(
',', $this->fieldNamesWithAlias( $vars ) );
675 $options = (array)$options;
677 $useIndexByTable = $options[
'USE INDEX'] ?? [];
678 if ( !is_array( $useIndexByTable ) ) {
679 if ( count( $tables ) <= 1 ) {
680 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
682 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous USE INDEX ($fname)" );
683 ( $this->errorLogger )( $e );
687 $ignoreIndexByTable = $options[
'IGNORE INDEX'] ?? [];
688 if ( !is_array( $ignoreIndexByTable ) ) {
689 if ( count( $tables ) <= 1 ) {
690 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
692 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous IGNORE INDEX ($fname)" );
693 ( $this->errorLogger )( $e );
698 $this->selectOptionsIncludeLocking( $options ) &&
699 $this->selectFieldsOrOptionsAggregate( $vars, $options )
703 $this->logger->warning(
704 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
708 if ( count( $tables ) ) {
709 $from =
' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
719 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
721 if ( is_array( $conds ) ) {
722 $where = $this->makeList( $conds, self::LIST_AND );
723 } elseif ( $conds ===
null || $conds ===
false ) {
725 $this->logger->warning(
729 .
' with incorrect parameters: $conds must be a string or an array',
730 [
'db_log_category' =>
'sql' ]
732 } elseif ( is_string( $conds ) ) {
735 throw new DBLanguageError( __METHOD__ .
' called with incorrect parameters' );
739 if ( $where ===
'' || $where ===
'*' ) {
740 $sql =
"SELECT $startOpts $fields $from $preLimitTail";
742 $sql =
"SELECT $startOpts $fields $from WHERE $where $preLimitTail";
745 if ( isset( $options[
'LIMIT'] ) ) {
746 $sql = $this->limitResult( $sql, $options[
'LIMIT'], $options[
'OFFSET'] ??
false );
748 $sql =
"$sql $postLimitTail";
750 if ( isset( $options[
'EXPLAIN'] ) ) {
751 $sql =
'EXPLAIN ' . $sql;
761 private function selectOptionsIncludeLocking( $options ) {
762 $options = (array)$options;
763 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
764 if ( in_array( $lock, $options,
true ) ) {
777 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
778 foreach ( (array)$options as $key => $value ) {
779 if ( is_string( $key ) ) {
780 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
783 } elseif ( is_string( $value ) ) {
784 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
790 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
791 foreach ( (array)$fields as $field ) {
792 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
808 foreach ( $fields as $alias => $field ) {
809 if ( is_numeric( $alias ) ) {
812 $retval[] = $this->fieldNameWithAlias( $field, $alias );
828 if ( !$alias || (
string)$alias === (
string)$name ) {
831 return $name .
' AS ' . $this->addIdentifierQuotes( $alias );
853 $use_index = (array)$use_index;
854 $ignore_index = (array)$ignore_index;
855 $join_conds = (array)$join_conds;
857 foreach ( $tables as $alias => $table ) {
858 if ( !is_string( $alias ) ) {
863 if ( is_array( $table ) ) {
865 if ( count( $table ) > 1 ) {
867 $this->tableNamesWithIndexClauseOrJOIN(
868 $table, $use_index, $ignore_index, $join_conds ) .
')';
871 $innerTable = reset( $table );
872 $innerAlias = key( $table );
873 $joinedTable = $this->tableNameWithAlias(
875 is_string( $innerAlias ) ? $innerAlias : $innerTable
879 $joinedTable = $this->tableNameWithAlias( $table, $alias );
883 if ( isset( $join_conds[$alias] ) ) {
884 Assert::parameterType(
'array', $join_conds[$alias],
"join_conds[$alias]" );
885 [ $joinType, $conds ] = $join_conds[$alias];
886 $tableClause = $this->normalizeJoinType( $joinType );
887 $tableClause .=
' ' . $joinedTable;
888 if ( isset( $use_index[$alias] ) ) {
889 $use = $this->useIndexClause( implode(
',', (array)$use_index[$alias] ) );
891 $tableClause .=
' ' . $use;
894 if ( isset( $ignore_index[$alias] ) ) {
895 $ignore = $this->ignoreIndexClause(
896 implode(
',', (array)$ignore_index[$alias] ) );
897 if ( $ignore !=
'' ) {
898 $tableClause .=
' ' . $ignore;
901 $on = $this->makeList( (array)$conds, self::LIST_AND );
903 $tableClause .=
' ON (' . $on .
')';
906 $retJOIN[] = $tableClause;
907 } elseif ( isset( $use_index[$alias] ) ) {
909 $tableClause = $joinedTable;
910 $tableClause .=
' ' . $this->useIndexClause(
911 implode(
',', (array)$use_index[$alias] )
914 $ret[] = $tableClause;
915 } elseif ( isset( $ignore_index[$alias] ) ) {
917 $tableClause = $joinedTable;
918 $tableClause .=
' ' . $this->ignoreIndexClause(
919 implode(
',', (array)$ignore_index[$alias] )
922 $ret[] = $tableClause;
924 $tableClause = $joinedTable;
926 $ret[] = $tableClause;
931 $implicitJoins = implode(
',', $ret );
932 $explicitJoins = implode(
' ', $retJOIN );
935 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
947 switch ( strtoupper( $joinType ) ) {
955 case 'STRAIGHT_JOIN':
956 case 'STRAIGHT JOIN':
977 if ( is_string( $table ) ) {
978 $quotedTable = $this->tableName( $table );
979 } elseif ( $table instanceof
Subquery ) {
980 $quotedTable = (string)$table;
982 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
985 if ( $alias ===
false || $alias === $table ) {
987 throw new InvalidArgumentException(
"Subquery table missing alias" );
992 return $quotedTable .
' ' . $this->addIdentifierQuotes( $alias );
1003 __METHOD__ .
': got Subquery instance when expecting a string'
1007 # Skip the entire process when we have a string quoted on both ends.
1008 # Note that we check the end so that we will still quote any use of
1009 # use of `database`.table. But won't break things if someone wants
1010 # to query a database table with a dot in the name.
1011 if ( $this->isQuotedIdentifier( $name ) ) {
1015 # Lets test for any bits of text that should never show up in a table
1016 # name. Basically anything like JOIN or ON which are actually part of
1017 # SQL queries, but may end up inside of the table value to combine
1018 # sql. Such as how the API is doing.
1019 # Note that we use a whitespace test rather than a \b test to avoid
1020 # any remote case where a word like on may be inside of a table name
1021 # surrounded by symbols which may be considered word breaks.
1022 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1023 $this->logger->warning(
1024 __METHOD__ .
": use of subqueries is not supported this way",
1026 'exception' =>
new RuntimeException(),
1027 'db_log_category' =>
'sql',
1034 # Split database and table into proper variables.
1035 [ $database, $schema, $prefix, $table ] = $this->qualifiedTableComponents( $name );
1037 # Quote $table and apply the prefix if not quoted.
1038 # $tableName might be empty if this is called from Database::replaceVars()
1039 $tableName =
"{$prefix}{$table}";
1040 if ( $format ===
'quoted'
1041 && !$this->isQuotedIdentifier( $tableName )
1042 && $tableName !==
''
1044 $tableName = $this->addIdentifierQuotes( $tableName );
1047 # Quote $schema and $database and merge them with the table name if needed
1048 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
1049 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
1061 # We reverse the explode so that database.table and table both output the correct table.
1062 $dbDetails = explode(
'.', $name, 3 );
1063 if ( $this->currentDomain ) {
1064 $currentDomainPrefix = $this->currentDomain->getTablePrefix();
1066 $currentDomainPrefix =
null;
1068 if ( count( $dbDetails ) == 3 ) {
1069 [ $database, $schema, $table ] = $dbDetails;
1070 # We don't want any prefix added in this case
1072 } elseif ( count( $dbDetails ) == 2 ) {
1073 [ $database, $table ] = $dbDetails;
1074 # We don't want any prefix added in this case
1076 # In dbs that support it, $database may actually be the schema
1077 # but that doesn't affect any of the functionality here
1080 [ $table ] = $dbDetails;
1081 if ( isset( $this->tableAliases[$table] ) ) {
1082 $database = $this->tableAliases[$table][
'dbname'];
1083 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1084 ? $this->tableAliases[$table][
'schema']
1085 : $this->relationSchemaQualifier();
1086 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1087 ? $this->tableAliases[$table][
'prefix']
1088 : $currentDomainPrefix;
1091 $schema = $this->relationSchemaQualifier(); # Default schema
1092 $prefix = $currentDomainPrefix; # Default prefix
1096 return [ $database, $schema, $prefix, $table ];
1104 if ( $this->currentDomain ) {
1105 return $this->currentDomain->getSchema();
1116 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1117 if ( $namespace !==
null && $namespace !==
'' ) {
1118 if ( $format ===
'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
1119 $namespace = $this->addIdentifierQuotes( $namespace );
1121 $relation = $namespace .
'.' . $relation;
1130 foreach ( $tables as $name ) {
1131 $retVal[$name] = $this->tableName( $name );
1140 foreach ( $tables as $name ) {
1141 $retVal[] = $this->tableName( $name );
1158 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
1203 $preLimitTail = $postLimitTail =
'';
1208 foreach ( $options as $key => $option ) {
1209 if ( is_numeric( $key ) ) {
1210 $noKeyOptions[$option] =
true;
1214 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1216 $preLimitTail .= $this->makeOrderBy( $options );
1218 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1219 $postLimitTail .=
' FOR UPDATE';
1222 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1223 $postLimitTail .=
' LOCK IN SHARE MODE';
1226 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1227 $startOpts .=
'DISTINCT';
1230 # Various MySQL extensions
1231 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1232 $startOpts .=
' /*! STRAIGHT_JOIN */';
1235 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1236 $startOpts .=
' SQL_BIG_RESULT';
1239 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1240 $startOpts .=
' SQL_BUFFER_RESULT';
1243 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1244 $startOpts .=
' SQL_SMALL_RESULT';
1247 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1248 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1251 return [ $startOpts, $preLimitTail, $postLimitTail ];
1264 if ( isset( $options[
'GROUP BY'] ) ) {
1265 $gb = is_array( $options[
'GROUP BY'] )
1266 ? implode(
',', $options[
'GROUP BY'] )
1267 : $options[
'GROUP BY'];
1268 $sql .=
' GROUP BY ' . $gb;
1270 if ( isset( $options[
'HAVING'] ) ) {
1271 $having = is_array( $options[
'HAVING'] )
1272 ? $this->makeList( $options[
'HAVING'], self::LIST_AND )
1273 : $options[
'HAVING'];
1274 $sql .=
' HAVING ' . $having;
1289 if ( isset( $options[
'ORDER BY'] ) ) {
1290 $ob = is_array( $options[
'ORDER BY'] )
1291 ? implode(
',', $options[
'ORDER BY'] )
1292 : $options[
'ORDER BY'];
1294 return ' ORDER BY ' . $ob;
1303 array $permute_conds,
1305 $fname = __METHOD__,
1311 foreach ( $permute_conds as $field => $values ) {
1316 $values = array_unique( $values );
1318 foreach ( $conds as $cond ) {
1319 foreach ( $values as $value ) {
1320 $cond[$field] = $value;
1321 $newConds[] = $cond;
1327 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
1331 if ( count( $conds ) === 1 &&
1332 ( !isset( $options[
'INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
1334 return $this->selectSQLText(
1335 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
1343 $orderBy = $this->makeOrderBy( $options );
1344 $limit = $options[
'LIMIT'] ??
null;
1345 $offset = $options[
'OFFSET'] ??
false;
1346 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
1347 if ( !$this->unionSupportsOrderAndLimit() ) {
1348 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
1350 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
1351 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
1353 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
1357 $options[
'LIMIT'] = $limit + $offset;
1358 unset( $options[
'OFFSET'] );
1363 foreach ( $conds as $cond ) {
1364 $sqls[] = $this->selectSQLText(
1365 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
1368 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
1369 if ( $limit !==
null ) {
1370 $sql = $this->limitResult( $sql, $limit, $offset );
1381 $delim, $table, $field, $conds =
'', $join_conds = []
1383 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) .
')';
1385 return '(' . $this->selectSQLText( $table, $fld, $conds, __METHOD__, [], $join_conds ) .
')';
1389 $table, $vars, $conds =
'', $fname = __METHOD__,
1390 $options = [], $join_conds = []
1393 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
1398 $encTable = $this->tableName( $table );
1399 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1401 return "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
1416 public function makeInsertLists( array $rows, $aliasPrefix =
'', array $typeByColumn = [] ) {
1417 $firstRow = $rows[0];
1418 if ( !is_array( $firstRow ) || !$firstRow ) {
1422 $tupleColumns = array_keys( $firstRow );
1425 foreach ( $rows as $row ) {
1426 $rowColumns = array_keys( $row );
1428 if ( $rowColumns !== $tupleColumns ) {
1430 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
1431 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
1435 $valueTuples[] =
'(' . $this->makeList( $row, self::LIST_COMMA ) .
')';
1438 $magicAliasFields = [];
1439 foreach ( $tupleColumns as $column ) {
1440 $magicAliasFields[] = $aliasPrefix . $column;
1444 $this->makeList( $tupleColumns, self::LIST_NAMES ),
1445 implode(
',', $valueTuples ),
1446 $this->makeList( $magicAliasFields, self::LIST_NAMES )
1451 $encTable = $this->tableName( $table );
1452 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1453 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1455 return rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
1464 return [
'INSERT IGNORE INTO',
'' ];
1473 array $insertOptions,
1474 array $selectOptions,
1477 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions(
'IGNORE', $insertOptions )
1478 ? $this->makeInsertNonConflictingVerbAndOptions()
1479 : [
'INSERT INTO',
'' ];
1480 $encDstTable = $this->tableName( $destTable );
1481 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
1482 $selectSql = $this->selectSQLText(
1484 array_values( $varMap ),
1491 return rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1501 foreach ( array_keys( $options, $option,
true ) as $k ) {
1502 if ( is_int( $k ) ) {
1520 } elseif ( !$uniqueKey ) {
1524 if ( count( $uniqueKey ) == 1 ) {
1526 $column = reset( $uniqueKey );
1527 $values = array_column( $rows, $column );
1528 if ( count( $values ) !== count( $rows ) ) {
1529 throw new DBLanguageError(
"Missing values for unique key ($column)" );
1532 return $this->makeList( [ $column => $values ], self::LIST_AND );
1535 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
1538 foreach ( $rows as $row ) {
1539 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1540 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1542 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
1545 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
1548 return count( $orConds ) > 1
1549 ? $this->makeList( $orConds, self::LIST_OR )
1555 throw new DBLanguageError( __METHOD__ .
' called with empty $conds' );
1558 $delTable = $this->tableName( $delTable );
1559 $joinTable = $this->tableName( $joinTable );
1560 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1561 if ( $conds !=
'*' ) {
1562 $sql .=
'WHERE ' . $this->makeList( $conds, self::LIST_AND );
1570 $this->assertConditionIsNotEmpty( $conds, __METHOD__,
false );
1572 $table = $this->tableName( $table );
1573 $sql =
"DELETE FROM $table";
1575 if ( $conds !== self::ALL_ROWS ) {
1576 if ( is_array( $conds ) ) {
1577 $conds = $this->makeList( $conds, self::LIST_AND );
1579 $sql .=
' WHERE ' . $conds;
1586 $this->assertConditionIsNotEmpty( $conds, __METHOD__,
true );
1587 $table = $this->tableName( $table );
1588 $opts = $this->makeUpdateOptions( $options );
1589 $sql =
"UPDATE $opts $table SET " . $this->makeList( $set, self::LIST_SET );
1591 if ( $conds && $conds !== self::ALL_ROWS ) {
1592 if ( is_array( $conds ) ) {
1593 $conds = $this->makeList( $conds, self::LIST_AND );
1595 $sql .=
' WHERE ' . $conds;
1615 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1616 if ( !$isCondValid ) {
1618 wfDeprecated( $fname .
' called with empty $conds',
'1.35',
false, 4 );
1620 throw new DBLanguageError( $fname .
' called with empty conditions' );
1633 $opts = $this->makeUpdateOptionsArray( $options );
1635 return implode(
' ', $opts );
1646 $options = $this->normalizeOptions( $options );
1650 if ( in_array(
'IGNORE', $options ) ) {
1663 if ( is_array( $options ) ) {
1665 } elseif ( is_string( $options ) ) {
1666 return ( $options ===
'' ) ? [] : [ $options ];
1668 throw new DBLanguageError( __METHOD__ .
': expected string or array' );
1676 return "DROP TABLE " . $this->tableName( $table ) .
" CASCADE";
1686 '/^\s*(rollback\s+to\s+savepoint|[a-z]+)/i',
1689 ) ? strtoupper( $m[1] ) :
null;
1708 $this->getQueryVerb( $sql ),
1712 'ROLLBACK TO SAVEPOINT',
1745 $this->fieldHasBit( $flags, self::QUERY_CHANGE_ROWS ) ||
1746 $this->fieldHasBit( $flags, self::QUERY_CHANGE_SCHEMA )
1752 $this->fieldHasBit( $flags, self::QUERY_CHANGE_NONE ) ||
1753 $this->fieldHasBit( $flags, self::QUERY_CHANGE_TRX ) ||
1754 $this->fieldHasBit( $flags, self::QUERY_CHANGE_LOCKS )
1759 $this->logger->warning( __METHOD__ .
' fallback to regex', [
1760 'exception' =>
new RuntimeException(),
1761 'db_log_category' =>
'sql',
1767 if ( preg_match(
'/^\s*\(?SELECT\b/i', $sql ) ) {
1768 return (
bool)preg_match(
'/\bFOR\s+UPDATE\)?\s*$/i', $sql );
1784 '/^\s*(BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE)\b/i',
1795 return ( ( $flags & $bit ) === $bit );
1801 return "(SELECT __$column FROM __VALS)";
1805 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1809 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1813 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1821 $rows = $this->normalizeRowArray( $rows );
1826 $options = $this->normalizeOptions( $options );
1827 if ( $this->isFlagInOptions(
'IGNORE', $options ) ) {
1828 return $this->insertNonConflictingSqlText( $table, $rows );
1830 return $this->insertSqlText( $table, $rows );
1840 if ( !$rowOrRows ) {
1842 } elseif ( isset( $rowOrRows[0] ) ) {
1845 $rows = [ $rowOrRows ];
1848 foreach ( $rows as $row ) {
1849 if ( !is_array( $row ) ) {
1851 } elseif ( !$row ) {
1869 $rows = $this->normalizeRowArray( $rows );
1873 if ( !$uniqueKeys ) {
1875 $this->logger->warning(
1876 "upsert/replace called with no unique key",
1878 'exception' =>
new RuntimeException(),
1879 'db_log_category' =>
'sql',
1884 $identityKey = $this->normalizeUpsertKeys( $uniqueKeys );
1885 if ( $identityKey ) {
1886 $allDefaultKeyValues = $this->assertValidUpsertRowArray( $rows, $identityKey );
1887 if ( $allDefaultKeyValues ) {
1890 $this->logger->warning(
1891 "upsert/replace called with all-null values for unique key",
1893 'exception' =>
new RuntimeException(),
1894 'db_log_category' =>
'sql',
1900 return $identityKey;
1910 if ( $conds ===
null || $conds ===
false ) {
1911 $this->logger->warning(
1915 .
' with incorrect parameters: $conds must be a string or an array',
1916 [
'db_log_category' =>
'sql' ]
1919 } elseif ( $conds ===
'' ) {
1923 return is_array( $conds ) ? $conds : [ $conds ];
1932 private function normalizeUpsertKeys( $uniqueKeys ) {
1933 if ( is_string( $uniqueKeys ) ) {
1934 return [ $uniqueKeys ];
1935 } elseif ( !is_array( $uniqueKeys ) ) {
1938 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1939 throw new DBLanguageError(
1940 "The unique key array should contain a single unique index" );
1943 $uniqueKey = $uniqueKeys[0];
1944 if ( is_string( $uniqueKey ) ) {
1947 $this->logger->warning( __METHOD__ .
1948 " called with deprecated parameter style: " .
1949 "the unique key array should be a string or array of string arrays",
1951 'exception' =>
new RuntimeException(),
1952 'db_log_category' =>
'sql',
1955 } elseif ( is_array( $uniqueKey ) ) {
1958 throw new DBLanguageError(
'Invalid unique key array entry' );
1971 foreach ( $rows as $row ) {
1972 foreach ( $identityKey as $column ) {
1973 $numNulls += ( isset( $row[$column] ) ? 0 : 1 );
1979 $numNulls !== ( count( $rows ) * count( $identityKey ) )
1982 "NULL/absent values for unique key (" . implode(
',', $identityKey ) .
")"
1986 return (
bool)$numNulls;
2001 throw new DBLanguageError(
"Update assignment list can't be empty for upsert" );
2006 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
2010 foreach ( $set as $k => $v ) {
2011 if ( is_string( $k ) ) {
2013 if ( in_array( $k, $identityKey,
true ) ) {
2014 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
2015 $this->logger->warning(
2016 __METHOD__ .
" called with redundant assignment to column '$k'",
2018 'exception' =>
new RuntimeException(),
2019 'db_log_category' =>
'sql',
2024 "Cannot reassign column '$k' since it belongs to identity key"
2028 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
2030 if ( in_array( $m[1], $identityKey,
true ) ) {
2032 "Cannot reassign column '{$m[1]}' since it belongs to identity key"
2044 if ( is_array( $var ) ) {
2047 } elseif ( count( $var ) == 1 ) {
2048 $column = $var[0] ?? reset( $var );
2060 $this->schemaVars = is_array( $vars ) ? $vars :
null;
2070 return $this->schemaVars ?? $this->getDefaultSchemaVars();
2108 $vars = $this->getSchemaVars();
2109 return preg_replace_callback(
2111 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
2112 \'\{\$ (\w+) }\' | # 3. addQuotes
2113 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
2114 /\*\$ (\w+) \*/ # 5. leave unencoded
2116 function ( $m ) use ( $vars ) {
2119 if ( isset( $m[1] ) && $m[1] !==
'' ) {
2120 if ( $m[1] ===
'i' ) {
2121 return $this->indexName( $m[2] );
2123 return $this->tableName( $m[2] );
2125 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
2126 return $this->quoter->addQuotes( $vars[$m[3]] );
2127 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
2128 return $this->addIdentifierQuotes( $vars[$m[4]] );
2129 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
2130 return $vars[$m[5]];
2140 throw new RuntimeException(
'locking must be implemented in subclasses' );
2144 throw new RuntimeException(
'locking must be implemented in subclasses' );
2148 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'))
The persistent session ID (if any) loaded at startup.
Class to handle database/schema/prefix specifications for IDatabase.