MediaWiki master
SQLPlatform.php
Go to the documentation of this file.
1<?php
21
22use InvalidArgumentException;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
25use RuntimeException;
26use Throwable;
27use Wikimedia\Assert\Assert;
38use Wikimedia\Timestamp\ConvertibleTimestamp;
39
47class SQLPlatform implements ISQLPlatform {
49 protected $tableAliases = [];
51 protected $indexAliases = [];
53 protected $currentDomain;
55 protected $schemaVars;
57 protected $quoter;
59 protected $logger;
61 protected $errorLogger;
62
63 public function __construct(
65 ?LoggerInterface $logger = null,
67 $errorLogger = null
68
69 ) {
70 $this->quoter = $quoter;
71 $this->logger = $logger ?? new NullLogger();
72 $this->currentDomain = $currentDomain ?: DatabaseDomain::newUnspecified();
73 $this->errorLogger = $errorLogger ?? static function ( Throwable $e ) {
74 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
75 };
76 }
77
78 public function bitNot( $field ) {
79 return "(~$field)";
80 }
81
82 public function bitAnd( $fieldLeft, $fieldRight ) {
83 return "($fieldLeft & $fieldRight)";
84 }
85
86 public function bitOr( $fieldLeft, $fieldRight ) {
87 return "($fieldLeft | $fieldRight)";
88 }
89
90 public function addIdentifierQuotes( $s ) {
91 if ( strcspn( $s, "\0\"`'." ) !== strlen( $s ) ) {
92 throw new DBLanguageError(
93 "Identifier must not contain quote, dot or null characters"
94 );
95 }
96 $quoteChar = $this->getIdentifierQuoteChar();
97 return $quoteChar . $s . $quoteChar;
98 }
99
104 protected function getIdentifierQuoteChar() {
105 return '"';
106 }
107
111 public function buildGreatest( $fields, $values ) {
112 return $this->buildSuperlative( 'GREATEST', $fields, $values );
113 }
114
118 public function buildLeast( $fields, $values ) {
119 return $this->buildSuperlative( 'LEAST', $fields, $values );
120 }
121
136 protected function buildSuperlative( $sqlfunc, $fields, $values ) {
137 $fields = is_array( $fields ) ? $fields : [ $fields ];
138 $values = is_array( $values ) ? $values : [ $values ];
139
140 $encValues = [];
141 foreach ( $fields as $alias => $field ) {
142 if ( is_int( $alias ) ) {
143 $encValues[] = $this->addIdentifierQuotes( $field );
144 } else {
145 $encValues[] = $field; // expression
146 }
147 }
148 foreach ( $values as $value ) {
149 if ( is_int( $value ) || is_float( $value ) ) {
150 $encValues[] = $value;
151 } elseif ( is_string( $value ) ) {
152 $encValues[] = $this->quoter->addQuotes( $value );
153 } elseif ( $value === null ) {
154 throw new DBLanguageError( 'Null value in superlative' );
155 } else {
156 throw new DBLanguageError( 'Unexpected value type in superlative' );
157 }
158 }
159
160 return $sqlfunc . '(' . implode( ',', $encValues ) . ')';
161 }
162
163 public function buildComparison( string $op, array $conds ): string {
164 if ( !in_array( $op, [ '>', '>=', '<', '<=' ] ) ) {
165 throw new InvalidArgumentException( "Comparison operator must be one of '>', '>=', '<', '<='" );
166 }
167 if ( count( $conds ) === 0 ) {
168 throw new InvalidArgumentException( "Empty input" );
169 }
170
171 // Construct a condition string by starting with the least significant part of the index, and
172 // adding more significant parts progressively to the left of the string.
173 //
174 // For example, given $conds = [ 'a' => 4, 'b' => 7, 'c' => 1 ], this will generate a condition
175 // like this:
176 //
177 // WHERE a > 4
178 // OR (a = 4 AND (b > 7
179 // OR (b = 7 AND (c > 1))))
180 //
181 // …which is equivalent to the following, which might be easier to understand:
182 //
183 // WHERE a > 4
184 // OR a = 4 AND b > 7
185 // OR a = 4 AND b = 7 AND c > 1
186 //
187 // …and also equivalent to the following, using tuple comparison syntax, which is most intuitive
188 // but apparently performs worse:
189 //
190 // WHERE (a, b, c) > (4, 7, 1)
191
192 $sql = '';
193 foreach ( array_reverse( $conds ) as $field => $value ) {
194 if ( is_int( $field ) ) {
195 throw new InvalidArgumentException(
196 'Non-associative array passed to buildComparison() (typo?)'
197 );
198 }
199 $encValue = $this->quoter->addQuotes( $value );
200 if ( $sql === '' ) {
201 $sql = "$field $op $encValue";
202 // Change '>=' to '>' etc. for remaining fields, as the equality is handled separately
203 $op = rtrim( $op, '=' );
204 } else {
205 $sql = "$field $op $encValue OR ($field = $encValue AND ($sql))";
206 }
207 }
208 return $sql;
209 }
210
211 public function makeList( array $a, $mode = self::LIST_COMMA ) {
212 $first = true;
213 $list = '';
214 $keyWarning = null;
215
216 foreach ( $a as $field => $value ) {
217 if ( $first ) {
218 $first = false;
219 } else {
220 if ( $mode == self::LIST_AND ) {
221 $list .= ' AND ';
222 } elseif ( $mode == self::LIST_OR ) {
223 $list .= ' OR ';
224 } else {
225 $list .= ',';
226 }
227 }
228
229 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
230 if ( $value instanceof IExpression ) {
231 $list .= "(" . $value->toSql( $this->quoter ) . ")";
232 } elseif ( is_array( $value ) ) {
233 throw new InvalidArgumentException( __METHOD__ . ": unexpected array value without key" );
234 } elseif ( $value instanceof RawSQLValue ) {
235 throw new InvalidArgumentException( __METHOD__ . ": unexpected raw value without key" );
236 } else {
237 $list .= "($value)";
238 }
239 } elseif ( $value instanceof IExpression ) {
240 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
241 throw new InvalidArgumentException( __METHOD__ . ": unexpected key $field for IExpression value" );
242 } else {
243 throw new InvalidArgumentException( __METHOD__ . ": unexpected IExpression outside WHERE clause" );
244 }
245 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
246 $list .= "$value";
247 } elseif (
248 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
249 ) {
250 // Remove null from array to be handled separately if found
251 $includeNull = false;
252 foreach ( array_keys( $value, null, true ) as $nullKey ) {
253 $includeNull = true;
254 unset( $value[$nullKey] );
255 }
256 if ( count( $value ) == 0 && !$includeNull ) {
257 throw new InvalidArgumentException(
258 __METHOD__ . ": empty input for field $field" );
259 } elseif ( count( $value ) == 0 ) {
260 // only check if $field is null
261 $list .= "$field IS NULL";
262 } else {
263 // IN clause contains at least one valid element
264 if ( $includeNull ) {
265 // Group subconditions to ensure correct precedence
266 $list .= '(';
267 }
268 if ( count( $value ) == 1 ) {
269 // Special-case single values, as IN isn't terribly efficient
270 // (but call makeList() so that warnings are emitted if needed)
271 $list .= $field . " = " . $this->makeList( $value );
272 } else {
273 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
274 }
275 // if null present in array, append IS NULL
276 if ( $includeNull ) {
277 $list .= " OR $field IS NULL)";
278 }
279 }
280 } elseif ( is_array( $value ) ) {
281 throw new InvalidArgumentException( __METHOD__ . ": unexpected nested array" );
282 } elseif ( $value === null ) {
283 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
284 $list .= "$field IS ";
285 } elseif ( $mode == self::LIST_SET ) {
286 $list .= "$field = ";
287 } elseif ( $mode === self::LIST_COMMA && !is_numeric( $field ) ) {
288 $keyWarning ??= [
289 __METHOD__ . ": array key {key} in list of values ignored",
290 [ 'key' => $field, 'exception' => new RuntimeException() ]
291 ];
292 } elseif ( $mode === self::LIST_NAMES && !is_numeric( $field ) ) {
293 $keyWarning ??= [
294 __METHOD__ . ": array key {key} in list of fields ignored",
295 [ 'key' => $field, 'exception' => new RuntimeException() ]
296 ];
297 }
298 $list .= 'NULL';
299 } else {
300 if (
301 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
302 ) {
303 $list .= "$field = ";
304 } elseif ( $mode === self::LIST_COMMA && !is_numeric( $field ) ) {
305 $keyWarning ??= [
306 __METHOD__ . ": array key {key} in list of values ignored",
307 [ 'key' => $field, 'exception' => new RuntimeException() ]
308 ];
309 } elseif ( $mode === self::LIST_NAMES && !is_numeric( $field ) ) {
310 $keyWarning ??= [
311 __METHOD__ . ": array key {key} in list of fields ignored",
312 [ 'key' => $field, 'exception' => new RuntimeException() ]
313 ];
314 }
315 $list .= $mode == self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
316 }
317 }
318
319 if ( $keyWarning ) {
320 // Only log one warning about this per function call, to reduce log spam when a dynamically
321 // generated associative array is passed
322 $this->logger->warning( ...$keyWarning );
323 }
324
325 return $list;
326 }
327
328 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
329 $conds = [];
330 foreach ( $data as $base => $sub ) {
331 if ( count( $sub ) ) {
332 $conds[] = $this->makeList(
333 [ $baseKey => $base, $subKey => array_map( 'strval', array_keys( $sub ) ) ],
334 self::LIST_AND
335 );
336 }
337 }
338
339 if ( !$conds ) {
340 throw new InvalidArgumentException( "Data for $baseKey and $subKey must be non-empty" );
341 }
342
343 return $this->makeList( $conds, self::LIST_OR );
344 }
345
346 public function factorConds( $condsArray ) {
347 if ( count( $condsArray ) === 0 ) {
348 throw new InvalidArgumentException(
349 __METHOD__ . ": empty condition array" );
350 }
351 $condsByFieldSet = [];
352 foreach ( $condsArray as $conds ) {
353 if ( !count( $conds ) ) {
354 throw new InvalidArgumentException(
355 __METHOD__ . ": empty condition subarray" );
356 }
357 $fieldKey = implode( ',', array_keys( $conds ) );
358 $condsByFieldSet[$fieldKey][] = $conds;
359 }
360 $result = '';
361 foreach ( $condsByFieldSet as $conds ) {
362 if ( $result !== '' ) {
363 $result .= ' OR ';
364 }
365 $result .= $this->factorCondsWithCommonFields( $conds );
366 }
367 return $result;
368 }
369
377 private function factorCondsWithCommonFields( $condsArray ) {
378 $first = $condsArray[array_key_first( $condsArray )];
379 if ( count( $first ) === 1 ) {
380 // IN clause
381 $field = array_key_first( $first );
382 $values = [];
383 foreach ( $condsArray as $conds ) {
384 $values[] = $conds[$field];
385 }
386 return $this->makeList( [ $field => $values ], self::LIST_AND );
387 }
388
389 $field1 = array_key_first( $first );
390 $nullExpressions = [];
391 $expressionsByField1 = [];
392 foreach ( $condsArray as $conds ) {
393 $value1 = $conds[$field1];
394 unset( $conds[$field1] );
395 if ( $value1 === null ) {
396 $nullExpressions[] = $conds;
397 } else {
398 $expressionsByField1[$value1][] = $conds;
399 }
400
401 }
402 $wrap = false;
403 $result = '';
404 foreach ( $expressionsByField1 as $value1 => $expressions ) {
405 if ( $result !== '' ) {
406 $result .= ' OR ';
407 $wrap = true;
408 }
409 $factored = $this->factorCondsWithCommonFields( $expressions );
410 $result .= "($field1 = " . $this->quoter->addQuotes( $value1 ) .
411 " AND $factored)";
412 }
413 if ( count( $nullExpressions ) ) {
414 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
415 if ( $result !== '' ) {
416 $result .= ' OR ';
417 $wrap = true;
418 }
419 $result .= "($field1 IS NULL AND $factored)";
420 }
421 if ( $wrap ) {
422 return "($result)";
423 } else {
424 return $result;
425 }
426 }
427
432 public function buildConcat( $stringList ) {
433 return 'CONCAT(' . implode( ',', $stringList ) . ')';
434 }
435
436 public function limitResult( $sql, $limit, $offset = false ) {
437 if ( !is_numeric( $limit ) ) {
438 throw new DBLanguageError(
439 "Invalid non-numeric limit passed to " . __METHOD__
440 );
441 }
442 // This version works in MySQL and SQLite. It will very likely need to be
443 // overridden for most other RDBMS subclasses.
444 return "$sql LIMIT "
445 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
446 . "{$limit} ";
447 }
448
455 public function escapeLikeInternal( $s, $escapeChar = '`' ) {
456 return str_replace(
457 [ $escapeChar, '%', '_' ],
458 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
459 $s
460 );
461 }
462
463 public function buildLike( $param, ...$params ) {
464 if ( is_array( $param ) ) {
465 $params = $param;
466 } else {
467 $params = func_get_args();
468 }
469 // @phan-suppress-next-line PhanParamTooFewUnpack
470 $likeValue = new LikeValue( ...$params );
471
472 return ' LIKE ' . $likeValue->toSql( $this->quoter );
473 }
474
475 public function anyChar() {
476 return new LikeMatch( '_' );
477 }
478
479 public function anyString() {
480 return new LikeMatch( '%' );
481 }
482
487 public function unionSupportsOrderAndLimit() {
488 return true; // True for almost every DB supported
489 }
490
491 public function unionQueries( $sqls, $all, $options = [] ) {
492 $glue = $all ? ') UNION ALL (' : ') UNION (';
493
494 $sql = '(' . implode( $glue, $sqls ) . ')';
495 if ( !$this->unionSupportsOrderAndLimit() ) {
496 return $sql;
497 }
498 $sql .= $this->makeOrderBy( $options );
499 $limit = $options['LIMIT'] ?? null;
500 $offset = $options['OFFSET'] ?? false;
501 if ( $limit !== null ) {
502 $sql = $this->limitResult( $sql, $limit, $offset );
503 }
504
505 return $sql;
506 }
507
508 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
509 if ( is_array( $cond ) ) {
510 $cond = $this->makeList( $cond, self::LIST_AND );
511 }
512 if ( $cond instanceof IExpression ) {
513 $cond = $cond->toSql( $this->quoter );
514 }
515
516 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
517 }
518
519 public function strreplace( $orig, $old, $new ) {
520 return "REPLACE({$orig}, {$old}, {$new})";
521 }
522
523 public function timestamp( $ts = 0 ) {
524 $t = new ConvertibleTimestamp( $ts );
525 // Let errors bubble up to avoid putting garbage in the DB
526 return $t->getTimestamp( TS_MW );
527 }
528
529 public function timestampOrNull( $ts = null ) {
530 if ( $ts === null ) {
531 return null;
532 } else {
533 return $this->timestamp( $ts );
534 }
535 }
536
537 public function getInfinity() {
538 return 'infinity';
539 }
540
541 public function encodeExpiry( $expiry ) {
542 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
543 ? $this->getInfinity()
544 : $this->timestamp( $expiry );
545 }
546
547 public function decodeExpiry( $expiry, $format = TS_MW ) {
548 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
549 return 'infinity';
550 }
551
552 return ConvertibleTimestamp::convert( $format, $expiry );
553 }
554
559 public function buildSubstring( $input, $startPosition, $length = null ) {
560 $this->assertBuildSubstringParams( $startPosition, $length );
561 $functionBody = "$input FROM $startPosition";
562 if ( $length !== null ) {
563 $functionBody .= " FOR $length";
564 }
565 return 'SUBSTRING(' . $functionBody . ')';
566 }
567
580 protected function assertBuildSubstringParams( $startPosition, $length ) {
581 if ( $startPosition === 0 ) {
582 // The DBMSs we support use 1-based indexing here.
583 throw new InvalidArgumentException( 'Use 1 as $startPosition for the beginning of the string' );
584 }
585 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
586 throw new InvalidArgumentException(
587 '$startPosition must be a positive integer'
588 );
589 }
590 if ( !( ( is_int( $length ) && $length >= 0 ) || $length === null ) ) {
591 throw new InvalidArgumentException(
592 '$length must be null or an integer greater than or equal to 0'
593 );
594 }
595 }
596
597 public function buildStringCast( $field ) {
598 // In theory this should work for any standards-compliant
599 // SQL implementation, although it may not be the best way to do it.
600 return "CAST( $field AS CHARACTER )";
601 }
602
603 public function buildIntegerCast( $field ) {
604 return 'CAST( ' . $field . ' AS INTEGER )';
605 }
606
607 public function implicitOrderby() {
608 return true;
609 }
610
619 public function indexName( $index ) {
620 return $this->indexAliases[$index] ?? $index;
621 }
622
623 public function setTableAliases( array $aliases ) {
624 $this->tableAliases = $aliases;
625 }
626
627 public function setIndexAliases( array $aliases ) {
628 $this->indexAliases = $aliases;
629 }
630
634 public function getTableAliases() {
635 return $this->tableAliases;
636 }
637
638 public function setPrefix( $prefix ) {
639 $this->currentDomain = new DatabaseDomain(
640 $this->currentDomain->getDatabase(),
641 $this->currentDomain->getSchema(),
642 $prefix
643 );
644 }
645
646 public function setCurrentDomain( DatabaseDomain $currentDomain ) {
647 $this->currentDomain = $currentDomain;
648 }
649
654 public function getCurrentDomain() {
655 return $this->currentDomain;
656 }
657
658 public function selectSQLText(
659 $tables, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
660 ) {
661 if ( !is_array( $tables ) ) {
662 if ( $tables === '' || $tables === null || $tables === false ) {
663 $tables = [];
664 } elseif ( is_string( $tables ) ) {
665 $tables = [ $tables ];
666 } else {
667 throw new DBLanguageError( __METHOD__ . ' called with incorrect table parameter' );
668 }
669 }
670
671 if ( is_array( $vars ) ) {
672 $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
673 } else {
674 $fields = $vars;
675 }
676
677 $options = (array)$options;
678
679 $useIndexByTable = $options['USE INDEX'] ?? [];
680 if ( !is_array( $useIndexByTable ) ) {
681 if ( count( $tables ) <= 1 ) {
682 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
683 } else {
684 $e = new DBLanguageError( __METHOD__ . " got ambiguous USE INDEX ($fname)" );
685 ( $this->errorLogger )( $e );
686 }
687 }
688
689 $ignoreIndexByTable = $options['IGNORE INDEX'] ?? [];
690 if ( !is_array( $ignoreIndexByTable ) ) {
691 if ( count( $tables ) <= 1 ) {
692 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
693 } else {
694 $e = new DBLanguageError( __METHOD__ . " got ambiguous IGNORE INDEX ($fname)" );
695 ( $this->errorLogger )( $e );
696 }
697 }
698
699 if (
700 $this->selectOptionsIncludeLocking( $options ) &&
701 $this->selectFieldsOrOptionsAggregate( $vars, $options )
702 ) {
703 // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
704 // functions. Discourage use of such queries to encourage compatibility.
705 $this->logger->warning(
706 __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
707 );
708 }
709
710 if ( count( $tables ) ) {
711 $from = ' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
712 $tables,
713 $useIndexByTable,
714 $ignoreIndexByTable,
715 $join_conds
716 );
717 } else {
718 $from = '';
719 }
720
721 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
722
723 if ( is_array( $conds ) ) {
724 $where = $this->makeList( $conds, self::LIST_AND );
725 } elseif ( $conds instanceof IExpression ) {
726 $where = $conds->toSql( $this->quoter );
727 } elseif ( $conds === null || $conds === false ) {
728 $where = '';
729 $this->logger->warning(
730 __METHOD__
731 . ' called from '
732 . $fname
733 . ' with incorrect parameters: $conds must be a string or an array',
734 [ 'db_log_category' => 'sql' ]
735 );
736 } elseif ( is_string( $conds ) ) {
737 $where = $conds;
738 } else {
739 throw new DBLanguageError( __METHOD__ . ' called with incorrect parameters' );
740 }
741
742 // Keep historical extra spaces after FROM to avoid testing failures
743 if ( $where === '' || $where === '*' ) {
744 $sql = "SELECT $startOpts $fields $from $preLimitTail";
745 } else {
746 $sql = "SELECT $startOpts $fields $from WHERE $where $preLimitTail";
747 }
748
749 if ( isset( $options['LIMIT'] ) ) {
750 $sql = $this->limitResult( $sql, $options['LIMIT'], $options['OFFSET'] ?? false );
751 }
752 $sql = "$sql $postLimitTail";
753
754 if ( isset( $options['EXPLAIN'] ) ) {
755 $sql = 'EXPLAIN ' . $sql;
756 }
757
758 if (
759 $fname === static::CALLER_UNKNOWN ||
760 str_starts_with( $fname, 'Wikimedia\\Rdbms\\' ) ||
761 $fname === '{closure}'
762 ) {
763 $exception = new RuntimeException();
764
765 // Try to figure out and report the real caller
766 $caller = '';
767 foreach ( $exception->getTrace() as $call ) {
768 if ( str_ends_with( $call['file'] ?? '', 'Test.php' ) ) {
769 // Don't warn when called directly by test code, adding callers there is pointless
770 break;
771 } elseif ( str_starts_with( $call['class'] ?? '', 'Wikimedia\\Rdbms\\' ) ) {
772 // Keep looking for the caller of a rdbms method
773 } elseif ( str_ends_with( $call['class'] ?? '', 'SelectQueryBuilder' ) ) {
774 // Keep looking for the caller of any custom SelectQueryBuilder
775 } else {
776 // Warn about the external caller we found
777 $caller = implode( '::', array_filter( [ $call['class'] ?? null, $call['function'] ] ) );
778 break;
779 }
780 }
781
782 if ( $fname === '{closure}' ) {
783 // Someone did ->caller( __METHOD__ ) in a local function, e.g. in a callback to
784 // getWithSetCallback(), MWCallableUpdate or doAtomicSection(). That's not very helpful.
785 // Provide a more specific message. The caller has to be provided like this:
786 // $method = __METHOD__;
787 // function ( ... ) use ( $method ) { ... }
788 $warning = "SQL query with incorrect caller (__METHOD__ used inside a closure: {caller}): {sql}";
789 } else {
790 $warning = "SQL query did not specify the caller (guessed caller: {caller}): {sql}";
791 }
792
793 $this->logger->warning(
794 $warning,
795 [ 'sql' => $sql, 'caller' => $caller, 'exception' => $exception ]
796 );
797 }
798
799 return $sql;
800 }
801
806 private function selectOptionsIncludeLocking( $options ) {
807 $options = (array)$options;
808 foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
809 if ( in_array( $lock, $options, true ) ) {
810 return true;
811 }
812 }
813
814 return false;
815 }
816
822 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
823 foreach ( (array)$options as $key => $value ) {
824 if ( is_string( $key ) ) {
825 if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
826 return true;
827 }
828 } elseif ( is_string( $value ) ) {
829 if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
830 return true;
831 }
832 }
833 }
834
835 $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
836 foreach ( (array)$fields as $field ) {
837 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
838 return true;
839 }
840 }
841
842 return false;
843 }
844
851 protected function fieldNamesWithAlias( $fields ) {
852 $retval = [];
853 foreach ( $fields as $alias => $field ) {
854 if ( is_numeric( $alias ) ) {
855 $alias = $field;
856 }
857 $retval[] = $this->fieldNameWithAlias( $field, $alias );
858 }
859
860 return $retval;
861 }
862
872 public function fieldNameWithAlias( $name, $alias = false ) {
873 if ( !$alias || (string)$alias === (string)$name ) {
874 return $name;
875 } else {
876 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
877 }
878 }
879
891 $tables,
892 $use_index = [],
893 $ignore_index = [],
894 $join_conds = []
895 ) {
896 $ret = [];
897 $retJOIN = [];
898 $use_index = (array)$use_index;
899 $ignore_index = (array)$ignore_index;
900 $join_conds = (array)$join_conds;
901
902 foreach ( $tables as $alias => $table ) {
903 if ( !is_string( $alias ) ) {
904 // No alias? Set it equal to the table name
905 $alias = $table;
906 }
907
908 if ( is_array( $table ) ) {
909 // A parenthesized group
910 if ( count( $table ) > 1 ) {
911 $joinedTable = '(' .
912 $this->tableNamesWithIndexClauseOrJOIN(
913 $table, $use_index, $ignore_index, $join_conds ) . ')';
914 } else {
915 // Degenerate case
916 $innerTable = reset( $table );
917 $innerAlias = key( $table );
918 $joinedTable = $this->tableNameWithAlias(
919 $innerTable,
920 is_string( $innerAlias ) ? $innerAlias : $innerTable
921 );
922 }
923 } else {
924 $joinedTable = $this->tableNameWithAlias( $table, $alias );
925 }
926
927 // Is there a JOIN clause for this table?
928 if ( isset( $join_conds[$alias] ) ) {
929 Assert::parameterType( 'array', $join_conds[$alias], "join_conds[$alias]" );
930 [ $joinType, $conds ] = $join_conds[$alias];
931 $tableClause = $this->normalizeJoinType( $joinType );
932 $tableClause .= ' ' . $joinedTable;
933 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
934 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
935 if ( $use != '' ) {
936 $tableClause .= ' ' . $use;
937 }
938 }
939 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
940 $ignore = $this->ignoreIndexClause(
941 implode( ',', (array)$ignore_index[$alias] ) );
942 if ( $ignore != '' ) {
943 $tableClause .= ' ' . $ignore;
944 }
945 }
946 $on = $this->makeList( (array)$conds, self::LIST_AND );
947 if ( $on != '' ) {
948 $tableClause .= ' ON (' . $on . ')';
949 }
950
951 $retJOIN[] = $tableClause;
952 } elseif ( isset( $use_index[$alias] ) ) {
953 // Is there an INDEX clause for this table?
954 $tableClause = $joinedTable;
955 $tableClause .= ' ' . $this->useIndexClause(
956 implode( ',', (array)$use_index[$alias] )
957 );
958
959 $ret[] = $tableClause;
960 } elseif ( isset( $ignore_index[$alias] ) ) {
961 // Is there an INDEX clause for this table?
962 $tableClause = $joinedTable;
963 $tableClause .= ' ' . $this->ignoreIndexClause(
964 implode( ',', (array)$ignore_index[$alias] )
965 );
966
967 $ret[] = $tableClause;
968 } else {
969 $tableClause = $joinedTable;
970
971 $ret[] = $tableClause;
972 }
973 }
974
975 // We can't separate explicit JOIN clauses with ',', use ' ' for those
976 $implicitJoins = implode( ',', $ret );
977 $explicitJoins = implode( ' ', $retJOIN );
978
979 // Compile our final table clause
980 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
981 }
982
991 protected function normalizeJoinType( string $joinType ) {
992 switch ( strtoupper( $joinType ) ) {
993 case 'JOIN':
994 case 'INNER JOIN':
995 return 'JOIN';
996
997 case 'LEFT JOIN':
998 return 'LEFT JOIN';
999
1000 case 'STRAIGHT_JOIN':
1001 case 'STRAIGHT JOIN':
1002 // MySQL only
1003 return 'JOIN';
1004
1005 default:
1006 return $joinType;
1007 }
1008 }
1009
1021 protected function tableNameWithAlias( $table, $alias = false ) {
1022 if ( is_string( $table ) ) {
1023 $quotedTable = $this->tableName( $table );
1024 } elseif ( $table instanceof Subquery ) {
1025 $quotedTable = (string)$table;
1026 } else {
1027 throw new InvalidArgumentException( "Table must be a string or Subquery" );
1028 }
1029
1030 if ( $alias === false || $alias === $table ) {
1031 if ( $table instanceof Subquery ) {
1032 throw new InvalidArgumentException( "Subquery table missing alias" );
1033 }
1034
1035 return $quotedTable;
1036 } else {
1037 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
1038 }
1039 }
1040
1041 public function tableName( string $name, $format = 'quoted' ) {
1042 $prefix = $this->currentDomain->getTablePrefix();
1043
1044 // Warn about table names that look qualified
1045 if (
1046 (
1047 str_contains( $name, '.' ) &&
1048 !preg_match( '/^information_schema\.[a-z_0-9]+$/', $name )
1049 ) ||
1050 ( $prefix !== '' && str_starts_with( $name, $prefix ) )
1051 ) {
1052 $this->logger->warning(
1053 __METHOD__ . ' called with qualified table ' . $name,
1054 [ 'db_log_category' => 'sql' ]
1055 );
1056 }
1057
1058 // Extract necessary database, schema, table identifiers and quote them as needed
1059 $formattedComponents = [];
1060 foreach ( $this->qualifiedTableComponents( $name ) as $component ) {
1061 if ( $format === 'quoted' ) {
1062 $formattedComponents[] = $this->addIdentifierQuotes( $component );
1063 } else {
1064 $formattedComponents[] = $component;
1065 }
1066 }
1067
1068 return implode( '.', $formattedComponents );
1069 }
1070
1093 public function qualifiedTableComponents( $name ) {
1094 $identifiers = $this->extractTableNameComponents( $name );
1095 if ( count( $identifiers ) > 3 ) {
1096 throw new DBLanguageError( "Too many components in table name '$name'" );
1097 }
1098 // Table alias config and prefixes only apply to unquoted single-identifier names
1099 if ( count( $identifiers ) == 1 && !$this->isQuotedIdentifier( $identifiers[0] ) ) {
1100 [ $table ] = $identifiers;
1101 if ( isset( $this->tableAliases[$table] ) ) {
1102 // This is an "alias" table that uses a different db/schema/prefix scheme
1103 $database = $this->tableAliases[$table]['dbname'];
1104 $schema = is_string( $this->tableAliases[$table]['schema'] )
1105 ? $this->tableAliases[$table]['schema']
1106 : $this->relationSchemaQualifier();
1107 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
1108 ? $this->tableAliases[$table]['prefix']
1109 : $this->currentDomain->getTablePrefix();
1110 } else {
1111 // Use the current database domain to resolve the schema and prefix
1112 $database = '';
1113 $schema = $this->relationSchemaQualifier();
1114 $prefix = $this->currentDomain->getTablePrefix();
1115 }
1116 $qualifierIdentifiers = [ $database, $schema ];
1117 $tableIdentifier = $prefix . $table;
1118 } else {
1119 $qualifierIdentifiers = array_slice( $identifiers, 0, -1 );
1120 $tableIdentifier = end( $identifiers );
1121 }
1122
1123 $components = [];
1124 foreach ( $qualifierIdentifiers as $identifier ) {
1125 if ( $identifier !== null && $identifier !== '' ) {
1126 $components[] = $this->isQuotedIdentifier( $identifier )
1127 ? substr( $identifier, 1, -1 )
1128 : $identifier;
1129 }
1130 }
1131 $components[] = $this->isQuotedIdentifier( $tableIdentifier )
1132 ? substr( $tableIdentifier, 1, -1 )
1133 : $tableIdentifier;
1134
1135 return $components;
1136 }
1137
1144 public function extractTableNameComponents( string $name ) {
1145 $quoteChar = $this->getIdentifierQuoteChar();
1146 $components = [];
1147 foreach ( explode( '.', $name ) as $component ) {
1148 if ( $this->isQuotedIdentifier( $component ) ) {
1149 $unquotedComponent = substr( $component, 1, -1 );
1150 } else {
1151 $unquotedComponent = $component;
1152 }
1153 if ( str_contains( $unquotedComponent, $quoteChar ) ) {
1154 throw new DBLanguageError(
1155 'Table name component contains unexpected quote or dot character' );
1156 }
1157 $components[] = $component;
1158 }
1159 return $components;
1160 }
1161
1187 public function getDatabaseAndTableIdentifier( string $table ) {
1188 $components = $this->qualifiedTableComponents( $table );
1189 switch ( count( $components ) ) {
1190 case 1:
1191 return [ $this->currentDomain->getDatabase(), $components[0] ];
1192 case 2:
1193 return $components;
1194 default:
1195 throw new DBLanguageError( 'Too many table components' );
1196 }
1197 }
1198
1203 protected function relationSchemaQualifier() {
1204 return $this->currentDomain->getSchema();
1205 }
1206
1210 public function tableNames( ...$tables ) {
1211 wfDeprecated( __METHOD__, '1.39' );
1212
1213 $retVal = [];
1214
1215 foreach ( $tables as $name ) {
1216 $retVal[$name] = $this->tableName( $name );
1217 }
1218
1219 return $retVal;
1220 }
1221
1222 public function tableNamesN( ...$tables ) {
1223 $retVal = [];
1224
1225 foreach ( $tables as $name ) {
1226 $retVal[] = $this->tableName( $name );
1227 }
1228
1229 return $retVal;
1230 }
1231
1241 public function isQuotedIdentifier( $name ) {
1242 $quoteChar = $this->getIdentifierQuoteChar();
1243 return strlen( $name ) > 1 && $name[0] === $quoteChar && $name[-1] === $quoteChar;
1244 }
1245
1259 public function useIndexClause( $index ) {
1260 return '';
1261 }
1262
1271 public function ignoreIndexClause( $index ) {
1272 return '';
1273 }
1274
1286 protected function makeSelectOptions( array $options ) {
1287 $preLimitTail = $postLimitTail = '';
1288 $startOpts = '';
1289
1290 $noKeyOptions = [];
1291
1292 foreach ( $options as $key => $option ) {
1293 if ( is_numeric( $key ) ) {
1294 $noKeyOptions[$option] = true;
1295 }
1296 }
1297
1298 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1299
1300 $preLimitTail .= $this->makeOrderBy( $options );
1301
1302 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1303 $postLimitTail .= ' FOR UPDATE';
1304 }
1305
1306 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1307 $postLimitTail .= ' LOCK IN SHARE MODE';
1308 }
1309
1310 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1311 $startOpts .= 'DISTINCT';
1312 }
1313
1314 # Various MySQL extensions
1315 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1316 $startOpts .= ' /*! STRAIGHT_JOIN */';
1317 }
1318
1319 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1320 $startOpts .= ' SQL_BIG_RESULT';
1321 }
1322
1323 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1324 $startOpts .= ' SQL_BUFFER_RESULT';
1325 }
1326
1327 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1328 $startOpts .= ' SQL_SMALL_RESULT';
1329 }
1330
1331 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1332 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1333 }
1334
1335 return [ $startOpts, $preLimitTail, $postLimitTail ];
1336 }
1337
1346 protected function makeGroupByWithHaving( $options ) {
1347 $sql = '';
1348 if ( isset( $options['GROUP BY'] ) ) {
1349 $gb = is_array( $options['GROUP BY'] )
1350 ? implode( ',', $options['GROUP BY'] )
1351 : $options['GROUP BY'];
1352 $sql .= ' GROUP BY ' . $gb;
1353 }
1354 if ( isset( $options['HAVING'] ) ) {
1355 $having = is_array( $options['HAVING'] )
1356 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1357 : $options['HAVING'];
1358 $sql .= ' HAVING ' . $having;
1359 }
1360
1361 return $sql;
1362 }
1363
1372 protected function makeOrderBy( $options ) {
1373 if ( isset( $options['ORDER BY'] ) ) {
1374 $ob = is_array( $options['ORDER BY'] )
1375 ? implode( ',', $options['ORDER BY'] )
1376 : $options['ORDER BY'];
1377
1378 return ' ORDER BY ' . $ob;
1379 }
1380
1381 return '';
1382 }
1383
1384 public function buildGroupConcatField(
1385 $delim, $tables, $field, $conds = '', $join_conds = []
1386 ) {
1387 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) . ')';
1388
1389 return '(' . $this->selectSQLText( $tables, $fld, $conds, static::CALLER_SUBQUERY, [], $join_conds ) . ')';
1390 }
1391
1392 public function buildSelectSubquery(
1393 $tables, $vars, $conds = '', $fname = __METHOD__,
1394 $options = [], $join_conds = []
1395 ) {
1396 return new Subquery(
1397 $this->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds )
1398 );
1399 }
1400
1401 public function insertSqlText( $table, array $rows ) {
1402 $encTable = $this->tableName( $table );
1403 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1404
1405 return [
1406 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1407 "INSERT INTO $encTable ($sqlColumns) VALUES '?'"
1408 ];
1409 }
1410
1423 public function makeInsertLists( array $rows, $aliasPrefix = '', array $typeByColumn = [] ) {
1424 $firstRow = $rows[0];
1425 if ( !is_array( $firstRow ) || !$firstRow ) {
1426 throw new DBLanguageError( 'Got an empty row list or empty row' );
1427 }
1428 // List of columns that define the value tuple ordering
1429 $tupleColumns = array_keys( $firstRow );
1430
1431 $valueTuples = [];
1432 foreach ( $rows as $row ) {
1433 $rowColumns = array_keys( $row );
1434 // VALUES(...) requires a uniform correspondence of (column => value)
1435 if ( $rowColumns !== $tupleColumns ) {
1436 throw new DBLanguageError(
1437 'Got row columns (' . implode( ', ', $rowColumns ) . ') ' .
1438 'instead of expected (' . implode( ', ', $tupleColumns ) . ')'
1439 );
1440 }
1441 // Make the value tuple that defines this row
1442 $valueTuples[] = '(' . $this->makeList( array_values( $row ), self::LIST_COMMA ) . ')';
1443 }
1444
1445 $magicAliasFields = [];
1446 foreach ( $tupleColumns as $column ) {
1447 $magicAliasFields[] = $aliasPrefix . $column;
1448 }
1449
1450 return [
1451 $this->makeList( $tupleColumns, self::LIST_NAMES ),
1452 implode( ',', $valueTuples ),
1453 $this->makeList( $magicAliasFields, self::LIST_NAMES )
1454 ];
1455 }
1456
1457 public function insertNonConflictingSqlText( $table, array $rows ) {
1458 $encTable = $this->tableName( $table );
1459 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1460 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1461
1462 return [
1463 rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" ),
1464 rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts" )
1465 ];
1466 }
1467
1474 return [ 'INSERT IGNORE INTO', '' ];
1475 }
1476
1478 $destTable,
1479 $srcTable,
1480 array $varMap,
1481 $conds,
1482 $fname,
1483 array $insertOptions,
1484 array $selectOptions,
1485 $selectJoinConds
1486 ) {
1487 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions( 'IGNORE', $insertOptions )
1488 ? $this->makeInsertNonConflictingVerbAndOptions()
1489 : [ 'INSERT INTO', '' ];
1490 $encDstTable = $this->tableName( $destTable );
1491 $sqlDstColumns = implode( ',', array_keys( $varMap ) );
1492 $selectSql = $this->selectSQLText(
1493 $srcTable,
1494 array_values( $varMap ),
1495 $conds,
1496 $fname,
1497 $selectOptions,
1498 $selectJoinConds
1499 );
1500
1501 return rtrim( "$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1502 }
1503
1510 public function isFlagInOptions( $option, array $options ) {
1511 foreach ( array_keys( $options, $option, true ) as $k ) {
1512 if ( is_int( $k ) ) {
1513 return true;
1514 }
1515 }
1516
1517 return false;
1518 }
1519
1527 public function makeKeyCollisionCondition( array $rows, array $uniqueKey ) {
1528 if ( !$rows ) {
1529 throw new DBLanguageError( "Empty row array" );
1530 } elseif ( !$uniqueKey ) {
1531 throw new DBLanguageError( "Empty unique key array" );
1532 }
1533
1534 if ( count( $uniqueKey ) == 1 ) {
1535 // Use a simple IN(...) clause
1536 $column = reset( $uniqueKey );
1537 $values = array_column( $rows, $column );
1538 if ( count( $values ) !== count( $rows ) ) {
1539 throw new DBLanguageError( "Missing values for unique key ($column)" );
1540 }
1541
1542 return $this->makeList( [ $column => $values ], self::LIST_AND );
1543 }
1544
1545 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey, null );
1546
1547 $orConds = [];
1548 foreach ( $rows as $row ) {
1549 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1550 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1551 throw new DBLanguageError(
1552 "Missing values for unique key (" . implode( ',', $uniqueKey ) . ")"
1553 );
1554 }
1555 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
1556 }
1557
1558 return count( $orConds ) > 1
1559 ? $this->makeList( $orConds, self::LIST_OR )
1560 : $orConds[0];
1561 }
1562
1563 public function deleteJoinSqlText( $delTable, $joinTable, $delVar, $joinVar, $conds ) {
1564 if ( !$conds ) {
1565 throw new DBLanguageError( __METHOD__ . ' called with empty $conds' );
1566 }
1567
1568 $delTable = $this->tableName( $delTable );
1569 $joinTable = $this->tableName( $joinTable );
1570 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1571 if ( $conds != '*' ) {
1572 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
1573 }
1574 $sql .= ')';
1575
1576 return $sql;
1577 }
1578
1584 public function deleteSqlText( $table, $conds ) {
1585 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1586 if ( !$isCondValid ) {
1587 throw new DBLanguageError( __METHOD__ . ' called with empty conditions' );
1588 }
1589
1590 $encTable = $this->tableName( $table );
1591 $sql = "DELETE FROM $encTable";
1592
1593 $condsSql = '';
1594 $cleanCondsSql = '';
1595 if ( $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1596 $cleanCondsSql = ' WHERE ' . $this->scrubArray( $conds );
1597 if ( is_array( $conds ) ) {
1598 $conds = $this->makeList( $conds, self::LIST_AND );
1599 }
1600 $condsSql .= ' WHERE ' . $conds;
1601 }
1602 return new Query(
1603 $sql . $condsSql,
1604 self::QUERY_CHANGE_ROWS,
1605 'DELETE',
1606 $table,
1607 $sql . $cleanCondsSql
1608 );
1609 }
1610
1611 private function scrubArray( $array, $listType = self::LIST_AND ) {
1612 if ( is_array( $array ) ) {
1613 $scrubbedArray = [];
1614 foreach ( $array as $key => $value ) {
1615 if ( $value instanceof IExpression ) {
1616 $scrubbedArray[$key] = $value->toGeneralizedSql();
1617 } else {
1618 $scrubbedArray[$key] = '?';
1619 }
1620 }
1621 return $this->makeList( $scrubbedArray, $listType );
1622 }
1623 return '?';
1624 }
1625
1626 public function updateSqlText( $table, $set, $conds, $options ) {
1627 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1628 if ( !$isCondValid ) {
1629 throw new DBLanguageError( __METHOD__ . ' called with empty conditions' );
1630 }
1631 $encTable = $this->tableName( $table );
1632 $opts = $this->makeUpdateOptions( $options );
1633 $sql = "UPDATE $opts $encTable";
1634 $condsSql = " SET " . $this->makeList( $set, self::LIST_SET );
1635 $cleanCondsSql = " SET " . $this->scrubArray( $set, self::LIST_SET );
1636
1637 if ( $conds && $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1638 $cleanCondsSql .= ' WHERE ' . $this->scrubArray( $conds );
1639 if ( is_array( $conds ) ) {
1640 $conds = $this->makeList( $conds, self::LIST_AND );
1641 }
1642 $condsSql .= ' WHERE ' . $conds;
1643 }
1644 return new Query(
1645 $sql . $condsSql,
1646 self::QUERY_CHANGE_ROWS,
1647 'UPDATE',
1648 $table,
1649 $sql . $cleanCondsSql
1650 );
1651 }
1652
1659 protected function makeUpdateOptions( $options ) {
1660 $opts = $this->makeUpdateOptionsArray( $options );
1661
1662 return implode( ' ', $opts );
1663 }
1664
1672 protected function makeUpdateOptionsArray( $options ) {
1673 $options = $this->normalizeOptions( $options );
1674
1675 $opts = [];
1676
1677 if ( in_array( 'IGNORE', $options ) ) {
1678 $opts[] = 'IGNORE';
1679 }
1680
1681 return $opts;
1682 }
1683
1689 final public function normalizeOptions( $options ) {
1690 if ( is_array( $options ) ) {
1691 return $options;
1692 } elseif ( is_string( $options ) ) {
1693 return ( $options === '' ) ? [] : [ $options ];
1694 } else {
1695 throw new DBLanguageError( __METHOD__ . ': expected string or array' );
1696 }
1697 }
1698
1699 public function dropTableSqlText( $table ) {
1700 // https://mariadb.com/kb/en/drop-table/
1701 // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html
1702 // https://www.postgresql.org/docs/9.2/sql-truncate.html
1703 return "DROP TABLE " . $this->tableName( $table ) . " CASCADE";
1704 }
1705
1711 public function getQueryVerb( $sql ) {
1712 wfDeprecated( __METHOD__, '1.42' );
1713 return QueryBuilderFromRawSql::buildQuery( $sql, 0 )->getVerb();
1714 }
1715
1728 public function isTransactableQuery( Query $sql ) {
1729 return !in_array(
1730 $sql->getVerb(),
1731 [
1732 'BEGIN',
1733 'ROLLBACK',
1734 'ROLLBACK TO SAVEPOINT',
1735 'COMMIT',
1736 'SET',
1737 'SHOW',
1738 'CREATE',
1739 'ALTER',
1740 'USE',
1741 'SHOW'
1742 ],
1743 true
1744 );
1745 }
1746
1747 public function buildExcludedValue( $column ) {
1748 /* @see Database::upsert() */
1749 // This can be treated like a single value since __VALS is a single row table
1750 return "(SELECT __$column FROM __VALS)";
1751 }
1752
1753 public function savepointSqlText( $identifier ) {
1754 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1755 }
1756
1757 public function releaseSavepointSqlText( $identifier ) {
1758 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1759 }
1760
1761 public function rollbackToSavepointSqlText( $identifier ) {
1762 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1763 }
1764
1765 public function rollbackSqlText() {
1766 return 'ROLLBACK';
1767 }
1768
1769 public function dispatchingInsertSqlText( $table, $rows, $options ) {
1770 $rows = $this->normalizeRowArray( $rows );
1771 if ( !$rows ) {
1772 return false;
1773 }
1774
1775 $options = $this->normalizeOptions( $options );
1776 if ( $this->isFlagInOptions( 'IGNORE', $options ) ) {
1777 [ $sql, $cleanSql ] = $this->insertNonConflictingSqlText( $table, $rows );
1778 } else {
1779 [ $sql, $cleanSql ] = $this->insertSqlText( $table, $rows );
1780 }
1781 return new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $table, $cleanSql );
1782 }
1783
1789 final protected function normalizeRowArray( array $rowOrRows ) {
1790 if ( !$rowOrRows ) {
1791 $rows = [];
1792 } elseif ( isset( $rowOrRows[0] ) ) {
1793 $rows = $rowOrRows;
1794 } else {
1795 $rows = [ $rowOrRows ];
1796 }
1797
1798 foreach ( $rows as $row ) {
1799 if ( !is_array( $row ) ) {
1800 throw new DBLanguageError( "Got non-array in row array" );
1801 } elseif ( !$row ) {
1802 throw new DBLanguageError( "Got empty array in row array" );
1803 }
1804 }
1805
1806 return $rows;
1807 }
1808
1817 final public function normalizeUpsertParams( $uniqueKeys, &$rows ) {
1818 $rows = $this->normalizeRowArray( $rows );
1819 if ( !$uniqueKeys ) {
1820 throw new DBLanguageError( 'No unique key specified for upsert/replace' );
1821 }
1822 $uniqueKey = $this->normalizeUpsertKeys( $uniqueKeys );
1823 $this->assertValidUpsertRowArray( $rows, $uniqueKey );
1824
1825 return $uniqueKey;
1826 }
1827
1834 final public function normalizeConditions( $conds, $fname ) {
1835 if ( $conds === null || $conds === false ) {
1836 $this->logger->warning(
1837 __METHOD__
1838 . ' called from '
1839 . $fname
1840 . ' with incorrect parameters: $conds must be a string or an array',
1841 [ 'db_log_category' => 'sql' ]
1842 );
1843 return [];
1844 } elseif ( $conds === '' ) {
1845 return [];
1846 }
1847
1848 return is_array( $conds ) ? $conds : [ $conds ];
1849 }
1850
1856 private function normalizeUpsertKeys( $uniqueKeys ) {
1857 if ( is_string( $uniqueKeys ) ) {
1858 return [ $uniqueKeys ];
1859 } elseif ( !is_array( $uniqueKeys ) ) {
1860 throw new DBLanguageError( 'Invalid unique key array' );
1861 } else {
1862 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1863 throw new DBLanguageError(
1864 "The unique key array should contain a single unique index" );
1865 }
1866
1867 $uniqueKey = $uniqueKeys[0];
1868 if ( is_string( $uniqueKey ) ) {
1869 // Passing a list of strings for single-column unique keys is too
1870 // easily confused with passing the columns of composite unique key
1871 $this->logger->warning( __METHOD__ .
1872 " called with deprecated parameter style: " .
1873 "the unique key array should be a string or array of string arrays",
1874 [
1875 'exception' => new RuntimeException(),
1876 'db_log_category' => 'sql',
1877 ] );
1878 return $uniqueKeys;
1879 } elseif ( is_array( $uniqueKey ) ) {
1880 return $uniqueKey;
1881 } else {
1882 throw new DBLanguageError( 'Invalid unique key array entry' );
1883 }
1884 }
1885 }
1886
1892 final protected function assertValidUpsertRowArray( array $rows, array $uniqueKey ) {
1893 foreach ( $rows as $row ) {
1894 foreach ( $uniqueKey as $column ) {
1895 if ( !isset( $row[$column] ) ) {
1896 throw new DBLanguageError(
1897 "NULL/absent values for unique key (" . implode( ',', $uniqueKey ) . ")"
1898 );
1899 }
1900 }
1901 }
1902 }
1903
1910 final public function assertValidUpsertSetArray(
1911 array $set,
1912 array $uniqueKey,
1913 array $rows
1914 ) {
1915 if ( !$set ) {
1916 throw new DBLanguageError( "Update assignment list can't be empty for upsert" );
1917 }
1918
1919 // Sloppy callers might construct the SET array using the ROW array, leaving redundant
1920 // column definitions for unique key columns. Detect this for backwards compatibility.
1921 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) : null;
1922 // Disallow value changes for any columns in the unique key. This avoids additional
1923 // insertion order dependencies that are unwieldy and difficult to implement efficiently
1924 // in PostgreSQL.
1925 foreach ( $set as $k => $v ) {
1926 if ( is_string( $k ) ) {
1927 // Key is a column name and value is a literal (e.g. string, int, null, ...)
1928 if ( in_array( $k, $uniqueKey, true ) ) {
1929 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1930 $this->logger->warning(
1931 __METHOD__ . " called with redundant assignment to column '$k'",
1932 [
1933 'exception' => new RuntimeException(),
1934 'db_log_category' => 'sql',
1935 ]
1936 );
1937 } else {
1938 throw new DBLanguageError(
1939 "Cannot reassign column '$k' since it belongs to the provided unique key"
1940 );
1941 }
1942 }
1943 } elseif ( preg_match( '/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1944 // Value is of the form "<unquoted alphanumeric column> = <SQL expression>"
1945 if ( in_array( $m[1], $uniqueKey, true ) ) {
1946 throw new DBLanguageError(
1947 "Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
1948 );
1949 }
1950 }
1951 }
1952 }
1953
1958 final public function extractSingleFieldFromList( $var ) {
1959 if ( is_array( $var ) ) {
1960 if ( !$var ) {
1961 $column = null;
1962 } elseif ( count( $var ) == 1 ) {
1963 $column = $var[0] ?? reset( $var );
1964 } else {
1965 throw new DBLanguageError( __METHOD__ . ': got multiple columns' );
1966 }
1967 } else {
1968 $column = $var;
1969 }
1970
1971 return $column;
1972 }
1973
1974 public function setSchemaVars( $vars ) {
1975 $this->schemaVars = is_array( $vars ) ? $vars : null;
1976 }
1977
1984 protected function getSchemaVars() {
1985 return $this->schemaVars ?? $this->getDefaultSchemaVars();
1986 }
1987
1997 protected function getDefaultSchemaVars() {
1998 return [];
1999 }
2000
2021 public function replaceVars( $ins ) {
2022 $vars = $this->getSchemaVars();
2023 return preg_replace_callback(
2024 '!
2025 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
2026 \'\{\$ (\w+) }\' | # 3. addQuotes
2027 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
2028 /\*\$ (\w+) \*/ # 5. leave unencoded
2029 !x',
2030 function ( $m ) use ( $vars ) {
2031 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
2032 // check for both nonexistent keys *and* the empty string.
2033 if ( isset( $m[1] ) && $m[1] !== '' ) {
2034 if ( $m[1] === 'i' ) {
2035 return $this->indexName( $m[2] );
2036 } else {
2037 return $this->tableName( $m[2] );
2038 }
2039 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
2040 return $this->quoter->addQuotes( $vars[$m[3]] );
2041 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
2042 return $this->addIdentifierQuotes( $vars[$m[4]] );
2043 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
2044 return $vars[$m[5]];
2045 } else {
2046 return $m[0];
2047 }
2048 },
2049 $ins
2050 );
2051 }
2052
2053 public function lockSQLText( $lockName, $timeout ) {
2054 throw new RuntimeException( 'locking must be implemented in subclasses' );
2055 }
2056
2057 public function lockIsFreeSQLText( $lockName ) {
2058 throw new RuntimeException( 'locking must be implemented in subclasses' );
2059 }
2060
2061 public function unlockSQLText( $lockName ) {
2062 throw new RuntimeException( 'locking must be implemented in subclasses' );
2063 }
2064}
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'))
Definition WebStart.php:81
Class to handle database/schema/prefix specifications for IDatabase.
Used by Database::buildLike() to represent characters that have special meaning in SQL LIKE clauses a...
Definition LikeMatch.php:10
Content of like value.
Definition LikeValue.php:14
buildGroupConcatField( $delim, $tables, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
decodeExpiry( $expiry, $format=TS_MW)
Decode an expiry time into a DBMS independent format.
getIdentifierQuoteChar()
Get the character used for identifier quoting.
anyChar()
Returns a token for buildLike() that denotes a '_' to be used in a LIKE query.
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.string
insertSelectNativeSqlText( $destTable, $srcTable, array $varMap, $conds, $fname, array $insertOptions, array $selectOptions, $selectJoinConds)
getInfinity()
Find out when 'infinity' is.
buildGreatest( $fields, $values)
Build a GREATEST function statement comparing columns/values.Integer and float values in $values will...
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
buildSelectSubquery( $tables, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subquery.
makeSelectOptions(array $options)
Returns an optional USE INDEX clause to go after the table, and a string to go at the end of the quer...
strreplace( $orig, $old, $new)
Returns a SQL expression for simple string replacement (e.g.
replaceVars( $ins)
Database-independent variable replacement.
normalizeUpsertParams( $uniqueKeys, &$rows)
Validate and normalize parameters to upsert() or replace()
setIndexAliases(array $aliases)
Convert certain index names to alternative names before querying the DB.
qualifiedTableComponents( $name)
Get the table components needed for a query given the currently selected database/schema.
makeKeyCollisionCondition(array $rows, array $uniqueKey)
Build an SQL condition to find rows with matching key values to those in $rows.
__construct(DbQuoter $quoter, ?LoggerInterface $logger=null, ?DatabaseDomain $currentDomain=null, $errorLogger=null)
array null $schemaVars
Current variables use for schema element placeholders.
ignoreIndexClause( $index)
IGNORE INDEX clause.
makeOrderBy( $options)
Returns an optional ORDER BY.
string[] $indexAliases
Current map of (index alias => index)
buildLike( $param,... $params)
LIKE statement wrapper.
bitOr( $fieldLeft, $fieldRight)
isTransactableQuery(Query $sql)
Determine whether a SQL statement is sensitive to isolation level.
fieldNameWithAlias( $name, $alias=false)
Get an aliased field name e.g.
buildComparison(string $op, array $conds)
Build a condition comparing multiple values, for use with indexes that cover multiple fields,...
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset.
unionSupportsOrderAndLimit()
Determine if the RDBMS supports ORDER BY and LIMIT for separate subqueries within UNION....
buildExcludedValue( $column)
Build a reference to a column value from the conflicting proposed upsert() row.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
buildSuperlative( $sqlfunc, $fields, $values)
Build a superlative function statement comparing columns/values.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
getDatabaseAndTableIdentifier(string $table)
Get the database identifer and prefixed table name identifier for a table.
assertValidUpsertSetArray(array $set, array $uniqueKey, array $rows)
bitAnd( $fieldLeft, $fieldRight)
array[] $tableAliases
Current map of (table => (dbname, schema, prefix) map)
normalizeJoinType(string $joinType)
Validate and normalize a join type.
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
buildSubstring( $input, $startPosition, $length=null)
unionQueries( $sqls, $all, $options=[])
Construct a UNION query.
getDefaultSchemaVars()
Get schema variables to use if none have been set via setSchemaVars().
factorConds( $condsArray)
Given an array of condition arrays representing an OR list of AND lists, for example:
dispatchingInsertSqlText( $table, $rows, $options)
updateSqlText( $table, $set, $conds, $options)
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
makeWhereFrom2d( $data, $baseKey, $subKey)
Build a "OR" condition with pairs from a two-dimensional array.
tableName(string $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
assertValidUpsertRowArray(array $rows, array $uniqueKey)
deleteJoinSqlText( $delTable, $joinTable, $delVar, $joinVar, $conds)
buildLeast( $fields, $values)
Build a LEAST function statement comparing columns/values.Integer and float values in $values will no...
makeInsertLists(array $rows, $aliasPrefix='', array $typeByColumn=[])
Make SQL lists of columns, row tuples, and column aliases for INSERT/VALUES expressions.
useIndexClause( $index)
USE INDEX clause.
encodeExpiry( $expiry)
Encode an expiry time into the DBMS dependent format.
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
isFlagInOptions( $option, array $options)
conditional( $cond, $caseTrueExpression, $caseFalseExpression)
Returns an SQL expression for a simple conditional.
setSchemaVars( $vars)
Set schema variables to be used when streaming commands from SQL files or stdin.
setCurrentDomain(DatabaseDomain $currentDomain)
selectSQLText( $tables, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
timestampOrNull( $ts=null)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
isQuotedIdentifier( $name)
Returns if the given identifier looks quoted or not according to the database convention for quoting ...
getSchemaVars()
Get schema variables.
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
insertNonConflictingSqlText( $table, array $rows)
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.
extractTableNameComponents(string $name)
Extract the dot-separated components of a table name, preserving identifier quotation.
escapeLikeInternal( $s, $escapeChar='`')
fieldNamesWithAlias( $fields)
Gets an array of aliased field names.
tableNamesN(... $tables)
Fetch a number of table names into a zero-indexed numerical array.
makeUpdateOptionsArray( $options)
Make UPDATE options array for Database::makeUpdateOptions.
tableNameWithAlias( $table, $alias=false)
Get an aliased table name.
callable $errorLogger
Error logging callback.
tableNamesWithIndexClauseOrJOIN( $tables, $use_index=[], $ignore_index=[], $join_conds=[])
Get the aliased table name clause for a FROM clause which might have a JOIN and/or USE INDEX or IGNOR...
This is to contain any regex on SQL work and get rid of them eventually.
static buildQuery(string $sql, $flags, string $tablePrefix='')
Holds information on Query to be executed.
Definition Query.php:31
Raw SQL value to be used in query builders.
Interface for query language.