22 use InvalidArgumentException;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\NullLogger;
27 use Wikimedia\Assert\Assert;
35 use Wikimedia\Timestamp\ConvertibleTimestamp;
68 $this->logger =
$logger ??
new NullLogger();
70 $this->errorLogger =
$errorLogger ??
static function ( Throwable $e ) {
71 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
87 public function bitAnd( $fieldLeft, $fieldRight ) {
88 return "($fieldLeft & $fieldRight)";
95 public function bitOr( $fieldLeft, $fieldRight ) {
96 return "($fieldLeft | $fieldRight)";
104 return '"' . str_replace(
'"',
'""', $s ) .
'"';
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))";
214 foreach ( $a as $field => $value ) {
235 $includeNull =
false;
236 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
238 unset( $value[$nullKey] );
240 if ( count( $value ) == 0 && !$includeNull ) {
241 throw new InvalidArgumentException(
242 __METHOD__ .
": empty input for field $field" );
243 } elseif ( count( $value ) == 0 ) {
245 $list .=
"$field IS NULL";
248 if ( $includeNull ) {
252 if ( count( $value ) == 1 ) {
256 $value = array_values( $value )[0];
257 $list .= $field .
" = " . $this->quoter->addQuotes( $value );
259 $list .= $field .
" IN (" . $this->makeList( $value ) .
") ";
262 if ( $includeNull ) {
263 $list .=
" OR $field IS NULL)";
266 } elseif ( $value ===
null ) {
268 $list .=
"$field IS ";
270 $list .=
"$field = ";
277 $list .=
"$field = ";
279 $list .= $mode ==
self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
288 foreach ( $data as $base => $sub ) {
289 if ( count( $sub ) ) {
290 $conds[] = $this->makeList(
291 [ $baseKey => $base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
298 throw new InvalidArgumentException(
"Data for $baseKey and $subKey must be non-empty" );
305 if ( count( $condsArray ) === 0 ) {
306 throw new InvalidArgumentException(
307 __METHOD__ .
": empty condition array" );
309 $condsByFieldSet = [];
310 foreach ( $condsArray as $conds ) {
311 if ( !count( $conds ) ) {
312 throw new InvalidArgumentException(
313 __METHOD__ .
": empty condition subarray" );
315 $fieldKey = implode(
',', array_keys( $conds ) );
316 $condsByFieldSet[$fieldKey][] = $conds;
319 foreach ( $condsByFieldSet as $conds ) {
320 if ( $result !==
'' ) {
323 $result .= $this->factorCondsWithCommonFields( $conds );
335 private function factorCondsWithCommonFields( $condsArray ) {
336 $first = $condsArray[array_key_first( $condsArray )];
337 if ( count( $first ) === 1 ) {
339 $field = array_key_first( $first );
341 foreach ( $condsArray as $conds ) {
342 $values[] = $conds[$field];
347 $field1 = array_key_first( $first );
348 $nullExpressions = [];
349 $expressionsByField1 = [];
350 foreach ( $condsArray as $conds ) {
351 $value1 = $conds[$field1];
352 unset( $conds[$field1] );
353 if ( $value1 ===
null ) {
354 $nullExpressions[] = $conds;
356 $expressionsByField1[$value1][] = $conds;
362 foreach ( $expressionsByField1 as $value1 => $expressions ) {
363 if ( $result !==
'' ) {
367 $factored = $this->factorCondsWithCommonFields( $expressions );
368 $result .=
"($field1 = " . $this->quoter->addQuotes( $value1 ) .
371 if ( count( $nullExpressions ) ) {
372 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
373 if ( $result !==
'' ) {
377 $result .=
"($field1 IS NULL AND $factored)";
391 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}_" ],
430 if ( is_array( $param ) ) {
433 $params = func_get_args();
444 foreach ( $params as $value ) {
446 $s .= $value->toString();
448 $s .= $this->escapeLikeInternal( $value, $escapeChar );
453 $this->quoter->addQuotes( $s ) .
' ESCAPE ' . $this->quoter->addQuotes( $escapeChar ) .
' ';
477 $glue = $all ?
') UNION ALL (' :
') UNION (';
479 $sql =
'(' . implode( $glue, $sqls ) .
')';
480 if ( !$this->unionSupportsOrderAndLimit() ) {
483 $sql = $sql . $this->makeOrderBy( $options );
484 $limit = $options[
'LIMIT'] ??
null;
485 $offset = $options[
'OFFSET'] ??
false;
486 if ( $limit !==
null ) {
487 $sql = $this->limitResult( $sql, $limit, $offset );
497 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
498 if ( is_array( $cond ) ) {
502 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
510 return "REPLACE({$orig}, {$old}, {$new})";
518 $t =
new ConvertibleTimestamp( $ts );
520 return $t->getTimestamp( TS_MW );
524 if ( $ts ===
null ) {
527 return $this->timestamp( $ts );
540 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() )
541 ? $this->getInfinity()
542 : $this->timestamp( $expiry );
546 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() ) {
550 return ConvertibleTimestamp::convert( $format, $expiry );
558 $this->assertBuildSubstringParams( $startPosition, $length );
559 $functionBody =
"$input FROM $startPosition";
560 if ( $length !==
null ) {
561 $functionBody .=
" FOR $length";
563 return 'SUBSTRING(' . $functionBody .
')';
579 if ( $startPosition === 0 ) {
581 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
583 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
584 throw new InvalidArgumentException(
585 '$startPosition must be a positive integer'
588 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
589 throw new InvalidArgumentException(
590 '$length must be null or an integer greater than or equal to 0'
602 return "CAST( $field AS CHARACTER )";
610 return 'CAST( ' . $field .
' AS INTEGER )';
630 return $this->indexAliases[$index] ?? $index;
638 $this->tableAliases = $aliases;
646 $this->indexAliases = $aliases;
653 return $this->tableAliases;
658 $this->currentDomain->getDatabase(),
659 $this->currentDomain->getSchema(),
665 $this->currentDomain = $currentDomain;
673 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
675 if ( is_array( $table ) ) {
677 } elseif ( $table ===
'' || $table ===
null || $table ===
false ) {
679 } elseif ( is_string( $table ) ) {
680 $tables = [ $table ];
682 throw new DBLanguageError( __METHOD__ .
' called with incorrect table parameter' );
685 if ( is_array( $vars ) ) {
686 $fields = implode(
',', $this->fieldNamesWithAlias( $vars ) );
691 $options = (array)$options;
693 $useIndexByTable = $options[
'USE INDEX'] ?? [];
694 if ( !is_array( $useIndexByTable ) ) {
695 if ( count( $tables ) <= 1 ) {
696 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
698 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous USE INDEX ($fname)" );
699 ( $this->errorLogger )( $e );
703 $ignoreIndexByTable = $options[
'IGNORE INDEX'] ?? [];
704 if ( !is_array( $ignoreIndexByTable ) ) {
705 if ( count( $tables ) <= 1 ) {
706 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
708 $e =
new DBLanguageError( __METHOD__ .
" got ambiguous IGNORE INDEX ($fname)" );
709 ( $this->errorLogger )( $e );
714 $this->selectOptionsIncludeLocking( $options ) &&
715 $this->selectFieldsOrOptionsAggregate( $vars, $options )
719 $this->logger->warning(
720 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
724 if ( count( $tables ) ) {
725 $from =
' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
735 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
737 if ( is_array( $conds ) ) {
739 } elseif ( $conds ===
null || $conds ===
false ) {
741 $this->logger->warning(
745 .
' with incorrect parameters: $conds must be a string or an array',
746 [
'db_log_category' =>
'sql' ]
748 } elseif ( is_string( $conds ) ) {
751 throw new DBLanguageError( __METHOD__ .
' called with incorrect parameters' );
755 if ( $where ===
'' || $where ===
'*' ) {
756 $sql =
"SELECT $startOpts $fields $from $preLimitTail";
758 $sql =
"SELECT $startOpts $fields $from WHERE $where $preLimitTail";
761 if ( isset( $options[
'LIMIT'] ) ) {
762 $sql = $this->limitResult( $sql, $options[
'LIMIT'], $options[
'OFFSET'] ??
false );
764 $sql =
"$sql $postLimitTail";
766 if ( isset( $options[
'EXPLAIN'] ) ) {
767 $sql =
'EXPLAIN ' . $sql;
777 private function selectOptionsIncludeLocking( $options ) {
778 $options = (array)$options;
779 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
780 if ( in_array( $lock, $options,
true ) ) {
793 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
794 foreach ( (array)$options as $key => $value ) {
795 if ( is_string( $key ) ) {
796 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
799 } elseif ( is_string( $value ) ) {
800 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
806 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
807 foreach ( (array)$fields as $field ) {
808 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
824 foreach ( $fields as $alias => $field ) {
825 if ( is_numeric( $alias ) ) {
828 $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;
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 || $alias === $table ) {
1002 if ( $table instanceof
Subquery ) {
1003 throw new InvalidArgumentException(
"Subquery table missing alias" );
1006 return $quotedTable;
1008 return $quotedTable .
' ' . $this->addIdentifierQuotes( $alias );
1019 __METHOD__ .
': got Subquery instance when expecting a string'
1023 # Skip the entire process when we have a string quoted on both ends.
1024 # Note that we check the end so that we will still quote any use of
1025 # use of `database`.table. But won't break things if someone wants
1026 # to query a database table with a dot in the name.
1027 if ( $this->isQuotedIdentifier( $name ) ) {
1031 # Lets test for any bits of text that should never show up in a table
1032 # name. Basically anything like JOIN or ON which are actually part of
1033 # SQL queries, but may end up inside of the table value to combine
1034 # sql. Such as how the API is doing.
1035 # Note that we use a whitespace test rather than a \b test to avoid
1036 # any remote case where a word like on may be inside of a table name
1037 # surrounded by symbols which may be considered word breaks.
1038 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1039 $this->logger->warning(
1040 __METHOD__ .
": use of subqueries is not supported this way",
1042 'exception' =>
new RuntimeException(),
1043 'db_log_category' =>
'sql',
1050 # Split database and table into proper variables.
1051 [ $database, $schema, $prefix, $table ] = $this->qualifiedTableComponents( $name );
1053 # Quote $table and apply the prefix if not quoted.
1054 # $tableName might be empty if this is called from Database::replaceVars()
1055 $tableName =
"{$prefix}{$table}";
1056 if ( $format ===
'quoted'
1057 && !$this->isQuotedIdentifier( $tableName )
1058 && $tableName !==
''
1060 $tableName = $this->addIdentifierQuotes( $tableName );
1063 # Quote $schema and $database and merge them with the table name if needed
1064 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
1065 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
1077 # We reverse the explode so that database.table and table both output the correct table.
1078 $dbDetails = explode(
'.', $name, 3 );
1079 if ( $this->currentDomain ) {
1080 $currentDomainPrefix = $this->currentDomain->getTablePrefix();
1082 $currentDomainPrefix =
null;
1084 if ( count( $dbDetails ) == 3 ) {
1085 [ $database, $schema, $table ] = $dbDetails;
1086 # We don't want any prefix added in this case
1088 } elseif ( count( $dbDetails ) == 2 ) {
1089 [ $database, $table ] = $dbDetails;
1090 # We don't want any prefix added in this case
1092 # In dbs that support it, $database may actually be the schema
1093 # but that doesn't affect any of the functionality here
1096 [ $table ] = $dbDetails;
1097 if ( isset( $this->tableAliases[$table] ) ) {
1098 $database = $this->tableAliases[$table][
'dbname'];
1099 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1100 ? $this->tableAliases[$table][
'schema']
1101 : $this->relationSchemaQualifier();
1102 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1103 ? $this->tableAliases[$table][
'prefix']
1104 : $currentDomainPrefix;
1107 $schema = $this->relationSchemaQualifier(); # Default schema
1108 $prefix = $currentDomainPrefix; # Default prefix
1112 return [ $database, $schema, $prefix, $table ];
1120 if ( $this->currentDomain ) {
1121 return $this->currentDomain->getSchema();
1132 private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1133 if ( $namespace !==
null && $namespace !==
'' ) {
1134 if ( $format ===
'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
1135 $namespace = $this->addIdentifierQuotes( $namespace );
1137 $relation = $namespace .
'.' . $relation;
1146 foreach ( $tables as $name ) {
1147 $retVal[$name] = $this->tableName( $name );
1156 foreach ( $tables as $name ) {
1157 $retVal[] = $this->tableName( $name );
1174 return strlen( $name ) > 1 && $name[0] ===
'"' && $name[-1] ===
'"';
1219 $preLimitTail = $postLimitTail =
'';
1224 foreach ( $options as $key => $option ) {
1225 if ( is_numeric( $key ) ) {
1226 $noKeyOptions[$option] =
true;
1230 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1232 $preLimitTail .= $this->makeOrderBy( $options );
1234 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1235 $postLimitTail .=
' FOR UPDATE';
1238 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1239 $postLimitTail .=
' LOCK IN SHARE MODE';
1242 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1243 $startOpts .=
'DISTINCT';
1246 # Various MySQL extensions
1247 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1248 $startOpts .=
' /*! STRAIGHT_JOIN */';
1251 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1252 $startOpts .=
' SQL_BIG_RESULT';
1255 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1256 $startOpts .=
' SQL_BUFFER_RESULT';
1259 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1260 $startOpts .=
' SQL_SMALL_RESULT';
1263 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1264 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1267 return [ $startOpts, $preLimitTail, $postLimitTail ];
1280 if ( isset( $options[
'GROUP BY'] ) ) {
1281 $gb = is_array( $options[
'GROUP BY'] )
1282 ? implode(
',', $options[
'GROUP BY'] )
1283 : $options[
'GROUP BY'];
1284 $sql .=
' GROUP BY ' . $gb;
1286 if ( isset( $options[
'HAVING'] ) ) {
1287 $having = is_array( $options[
'HAVING'] )
1289 : $options[
'HAVING'];
1290 $sql .=
' HAVING ' . $having;
1305 if ( isset( $options[
'ORDER BY'] ) ) {
1306 $ob = is_array( $options[
'ORDER BY'] )
1307 ? implode(
',', $options[
'ORDER BY'] )
1308 : $options[
'ORDER BY'];
1310 return ' ORDER BY ' . $ob;
1321 $delim, $table, $field, $conds =
'', $join_conds = []
1323 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) .
')';
1325 return '(' . $this->selectSQLText( $table, $fld, $conds, __METHOD__, [], $join_conds ) .
')';
1329 $table, $vars, $conds =
'', $fname = __METHOD__,
1330 $options = [], $join_conds = []
1333 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
1338 $encTable = $this->tableName( $table );
1339 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1342 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1343 "INSERT INTO $encTable ($sqlColumns) VALUES '?'"
1359 public function makeInsertLists( array $rows, $aliasPrefix =
'', array $typeByColumn = [] ) {
1360 $firstRow = $rows[0];
1361 if ( !is_array( $firstRow ) || !$firstRow ) {
1365 $tupleColumns = array_keys( $firstRow );
1368 foreach ( $rows as $row ) {
1369 $rowColumns = array_keys( $row );
1371 if ( $rowColumns !== $tupleColumns ) {
1373 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
1374 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
1381 $magicAliasFields = [];
1382 foreach ( $tupleColumns as $column ) {
1383 $magicAliasFields[] = $aliasPrefix . $column;
1388 implode(
',', $valueTuples ),
1394 $encTable = $this->tableName( $table );
1395 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1396 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1399 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" ),
1400 rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts" )
1410 return [
'INSERT IGNORE INTO',
'' ];
1419 array $insertOptions,
1420 array $selectOptions,
1423 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions(
'IGNORE', $insertOptions )
1424 ? $this->makeInsertNonConflictingVerbAndOptions()
1425 : [
'INSERT INTO',
'' ];
1426 $encDstTable = $this->tableName( $destTable );
1427 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
1428 $selectSql = $this->selectSQLText(
1430 array_values( $varMap ),
1437 return rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1447 foreach ( array_keys( $options, $option,
true ) as $k ) {
1448 if ( is_int( $k ) ) {
1466 } elseif ( !$uniqueKey ) {
1470 if ( count( $uniqueKey ) == 1 ) {
1472 $column = reset( $uniqueKey );
1473 $values = array_column( $rows, $column );
1474 if ( count( $values ) !== count( $rows ) ) {
1475 throw new DBLanguageError(
"Missing values for unique key ($column)" );
1478 return $this->makeList( [ $column => $values ],
self::LIST_AND );
1481 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
1484 foreach ( $rows as $row ) {
1485 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1486 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1488 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
1494 return count( $orConds ) > 1
1501 throw new DBLanguageError( __METHOD__ .
' called with empty $conds' );
1504 $delTable = $this->tableName( $delTable );
1505 $joinTable = $this->tableName( $joinTable );
1506 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1507 if ( $conds !=
'*' ) {
1521 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1522 if ( !$isCondValid ) {
1523 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1526 $encTable = $this->tableName( $table );
1527 $sql =
"DELETE FROM $encTable";
1530 $cleanCondsSql =
'';
1531 if ( $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1532 $cleanCondsSql =
' WHERE ' . $this->scrubArray( $conds );
1533 if ( is_array( $conds ) ) {
1536 $condsSql .=
' WHERE ' . $conds;
1540 self::QUERY_CHANGE_ROWS,
1543 $sql . $cleanCondsSql
1547 private function scrubArray( $array, $listType =
self::LIST_AND ) {
1548 if ( is_array( $array ) ) {
1549 $scrubbedArray = [];
1550 foreach ( $array as $key => $value ) {
1551 $scrubbedArray[$key] =
'?';
1553 return $this->makeList( $scrubbedArray, $listType );
1559 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1560 if ( !$isCondValid ) {
1561 throw new DBLanguageError( __METHOD__ .
' called with empty conditions' );
1563 $encTable = $this->tableName( $table );
1564 $opts = $this->makeUpdateOptions( $options );
1565 $sql =
"UPDATE $opts $encTable";
1567 $cleanCondsSql =
" SET " . $this->scrubArray( $set,
self::LIST_SET );
1569 if ( $conds && $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1570 $cleanCondsSql .=
' WHERE ' . $this->scrubArray( $conds );
1571 if ( is_array( $conds ) ) {
1574 $condsSql .=
' WHERE ' . $conds;
1578 self::QUERY_CHANGE_ROWS,
1581 $sql . $cleanCondsSql
1593 $opts = $this->makeUpdateOptionsArray( $options );
1595 return implode(
' ', $opts );
1606 $options = $this->normalizeOptions( $options );
1610 if ( in_array(
'IGNORE', $options ) ) {
1623 if ( is_array( $options ) ) {
1625 } elseif ( is_string( $options ) ) {
1626 return ( $options ===
'' ) ? [] : [ $options ];
1628 throw new DBLanguageError( __METHOD__ .
': expected string or array' );
1636 return "DROP TABLE " . $this->tableName( $table ) .
" CASCADE";
1666 'ROLLBACK TO SAVEPOINT',
1704 return "(SELECT __$column FROM __VALS)";
1708 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1712 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1716 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1724 $rows = $this->normalizeRowArray( $rows );
1729 $options = $this->normalizeOptions( $options );
1730 if ( $this->isFlagInOptions(
'IGNORE', $options ) ) {
1731 [ $sql, $cleanSql ] = $this->insertNonConflictingSqlText( $table, $rows );
1733 [ $sql, $cleanSql ] = $this->insertSqlText( $table, $rows );
1735 return new Query( $sql, self::QUERY_CHANGE_ROWS,
'INSERT', $table, $cleanSql );
1744 if ( !$rowOrRows ) {
1746 } elseif ( isset( $rowOrRows[0] ) ) {
1749 $rows = [ $rowOrRows ];
1752 foreach ( $rows as $row ) {
1753 if ( !is_array( $row ) ) {
1755 } elseif ( !$row ) {
1772 $rows = $this->normalizeRowArray( $rows );
1773 if ( !$uniqueKeys ) {
1774 throw new DBLanguageError(
'No unique key specified for upsert/replace' );
1776 $uniqueKey = $this->normalizeUpsertKeys( $uniqueKeys );
1777 $this->assertValidUpsertRowArray( $rows, $uniqueKey );
1789 if ( $conds ===
null || $conds ===
false ) {
1790 $this->logger->warning(
1794 .
' with incorrect parameters: $conds must be a string or an array',
1795 [
'db_log_category' =>
'sql' ]
1798 } elseif ( $conds ===
'' ) {
1802 return is_array( $conds ) ? $conds : [ $conds ];
1810 private function normalizeUpsertKeys( $uniqueKeys ) {
1811 if ( is_string( $uniqueKeys ) ) {
1812 return [ $uniqueKeys ];
1813 } elseif ( !is_array( $uniqueKeys ) ) {
1816 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1817 throw new DBLanguageError(
1818 "The unique key array should contain a single unique index" );
1821 $uniqueKey = $uniqueKeys[0];
1822 if ( is_string( $uniqueKey ) ) {
1825 $this->logger->warning( __METHOD__ .
1826 " called with deprecated parameter style: " .
1827 "the unique key array should be a string or array of string arrays",
1829 'exception' =>
new RuntimeException(),
1830 'db_log_category' =>
'sql',
1833 } elseif ( is_array( $uniqueKey ) ) {
1836 throw new DBLanguageError(
'Invalid unique key array entry' );
1847 foreach ( $rows as $row ) {
1848 foreach ( $uniqueKey as $column ) {
1849 if ( !isset( $row[$column] ) ) {
1851 "NULL/absent values for unique key (" . implode(
',', $uniqueKey ) .
")"
1870 throw new DBLanguageError(
"Update assignment list can't be empty for upsert" );
1875 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
1879 foreach ( $set as $k => $v ) {
1880 if ( is_string( $k ) ) {
1882 if ( in_array( $k, $uniqueKey,
true ) ) {
1883 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1884 $this->logger->warning(
1885 __METHOD__ .
" called with redundant assignment to column '$k'",
1887 'exception' =>
new RuntimeException(),
1888 'db_log_category' =>
'sql',
1893 "Cannot reassign column '$k' since it belongs to the provided unique key"
1897 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1899 if ( in_array( $m[1], $uniqueKey,
true ) ) {
1901 "Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
1913 if ( is_array( $var ) ) {
1916 } elseif ( count( $var ) == 1 ) {
1917 $column = $var[0] ?? reset( $var );
1929 $this->schemaVars = is_array( $vars ) ? $vars :
null;
1939 return $this->schemaVars ?? $this->getDefaultSchemaVars();
1977 $vars = $this->getSchemaVars();
1978 return preg_replace_callback(
1980 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
1981 \'\{\$ (\w+) }\' | # 3. addQuotes
1982 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
1983 /\*\$ (\w+) \*/ # 5. leave unencoded
1985 function ( $m ) use ( $vars ) {
1988 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1989 if ( $m[1] ===
'i' ) {
1990 return $this->indexName( $m[2] );
1992 return $this->tableName( $m[2] );
1994 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
1995 return $this->quoter->addQuotes( $vars[$m[3]] );
1996 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
1997 return $this->addIdentifierQuotes( $vars[$m[4]] );
1998 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
1999 return $vars[$m[5]];
2009 throw new RuntimeException(
'locking must be implemented in subclasses' );
2013 throw new RuntimeException(
'locking must be implemented in subclasses' );
2017 throw new RuntimeException(
'locking must be implemented in subclasses' );
if(!defined('MW_SETUP_CALLBACK'))
Class to handle database/schema/prefix specifications for IDatabase.