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;
37use Wikimedia\Timestamp\ConvertibleTimestamp;
38
46class SQLPlatform implements ISQLPlatform {
48 protected $tableAliases = [];
50 protected $indexAliases = [];
52 protected $currentDomain;
54 protected $schemaVars;
56 protected $quoter;
58 protected $logger;
60 protected $errorLogger;
61
62 public function __construct(
64 LoggerInterface $logger = null,
66 $errorLogger = null
67
68 ) {
69 $this->quoter = $quoter;
70 $this->logger = $logger ?? new NullLogger();
71 $this->currentDomain = $currentDomain ?: DatabaseDomain::newUnspecified();
72 $this->errorLogger = $errorLogger ?? static function ( Throwable $e ) {
73 trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
74 };
75 }
76
77 public function bitNot( $field ) {
78 return "(~$field)";
79 }
80
81 public function bitAnd( $fieldLeft, $fieldRight ) {
82 return "($fieldLeft & $fieldRight)";
83 }
84
85 public function bitOr( $fieldLeft, $fieldRight ) {
86 return "($fieldLeft | $fieldRight)";
87 }
88
89 public function addIdentifierQuotes( $s ) {
90 if ( strcspn( $s, "\0\"`'." ) !== strlen( $s ) ) {
91 throw new DBLanguageError(
92 "Identifier must not contain quote, dot or null characters"
93 );
94 }
95 $quoteChar = $this->getIdentifierQuoteChar();
96 return $quoteChar . $s . $quoteChar;
97 }
98
103 protected function getIdentifierQuoteChar() {
104 return '"';
105 }
106
110 public function buildGreatest( $fields, $values ) {
111 return $this->buildSuperlative( 'GREATEST', $fields, $values );
112 }
113
117 public function buildLeast( $fields, $values ) {
118 return $this->buildSuperlative( 'LEAST', $fields, $values );
119 }
120
135 protected function buildSuperlative( $sqlfunc, $fields, $values ) {
136 $fields = is_array( $fields ) ? $fields : [ $fields ];
137 $values = is_array( $values ) ? $values : [ $values ];
138
139 $encValues = [];
140 foreach ( $fields as $alias => $field ) {
141 if ( is_int( $alias ) ) {
142 $encValues[] = $this->addIdentifierQuotes( $field );
143 } else {
144 $encValues[] = $field; // expression
145 }
146 }
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 ) {
153 throw new DBLanguageError( 'Null value in superlative' );
154 } else {
155 throw new DBLanguageError( 'Unexpected value type in superlative' );
156 }
157 }
158
159 return $sqlfunc . '(' . implode( ',', $encValues ) . ')';
160 }
161
162 public function buildComparison( string $op, array $conds ): string {
163 if ( !in_array( $op, [ '>', '>=', '<', '<=' ] ) ) {
164 throw new InvalidArgumentException( "Comparison operator must be one of '>', '>=', '<', '<='" );
165 }
166 if ( count( $conds ) === 0 ) {
167 throw new InvalidArgumentException( "Empty input" );
168 }
169
170 // Construct a condition string by starting with the least significant part of the index, and
171 // adding more significant parts progressively to the left of the string.
172 //
173 // For example, given $conds = [ 'a' => 4, 'b' => 7, 'c' => 1 ], this will generate a condition
174 // like this:
175 //
176 // WHERE a > 4
177 // OR (a = 4 AND (b > 7
178 // OR (b = 7 AND (c > 1))))
179 //
180 // …which is equivalent to the following, which might be easier to understand:
181 //
182 // WHERE a > 4
183 // OR a = 4 AND b > 7
184 // OR a = 4 AND b = 7 AND c > 1
185 //
186 // …and also equivalent to the following, using tuple comparison syntax, which is most intuitive
187 // but apparently performs worse:
188 //
189 // WHERE (a, b, c) > (4, 7, 1)
190
191 $sql = '';
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?)'
196 );
197 }
198 $encValue = $this->quoter->addQuotes( $value );
199 if ( $sql === '' ) {
200 $sql = "$field $op $encValue";
201 // Change '>=' to '>' etc. for remaining fields, as the equality is handled separately
202 $op = rtrim( $op, '=' );
203 } else {
204 $sql = "$field $op $encValue OR ($field = $encValue AND ($sql))";
205 }
206 }
207 return $sql;
208 }
209
210 public function makeList( array $a, $mode = self::LIST_COMMA ) {
211 $first = true;
212 $list = '';
213
214 foreach ( $a as $field => $value ) {
215 if ( $first ) {
216 $first = false;
217 } else {
218 if ( $mode == self::LIST_AND ) {
219 $list .= ' AND ';
220 } elseif ( $mode == self::LIST_OR ) {
221 $list .= ' OR ';
222 } else {
223 $list .= ',';
224 }
225 }
226
227 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
228 if ( $value instanceof IExpression ) {
229 $list .= "(" . $value->toSql( $this->quoter ) . ")";
230 } else {
231 $list .= "($value)";
232 }
233 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
234 $list .= "$value";
235 } elseif (
236 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
237 ) {
238 // Remove null from array to be handled separately if found
239 $includeNull = false;
240 foreach ( array_keys( $value, null, true ) as $nullKey ) {
241 $includeNull = true;
242 unset( $value[$nullKey] );
243 }
244 if ( count( $value ) == 0 && !$includeNull ) {
245 throw new InvalidArgumentException(
246 __METHOD__ . ": empty input for field $field" );
247 } elseif ( count( $value ) == 0 ) {
248 // only check if $field is null
249 $list .= "$field IS NULL";
250 } else {
251 // IN clause contains at least one valid element
252 if ( $includeNull ) {
253 // Group subconditions to ensure correct precedence
254 $list .= '(';
255 }
256 if ( count( $value ) == 1 ) {
257 // Special-case single values, as IN isn't terribly efficient
258 // Don't necessarily assume the single key is 0; we don't
259 // enforce linear numeric ordering on other arrays here.
260 $value = array_values( $value )[0];
261 $list .= $field . " = " . $this->quoter->addQuotes( $value );
262 } else {
263 $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
264 }
265 // if null present in array, append IS NULL
266 if ( $includeNull ) {
267 $list .= " OR $field IS NULL)";
268 }
269 }
270 } elseif ( $value === null ) {
271 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
272 $list .= "$field IS ";
273 } elseif ( $mode == self::LIST_SET ) {
274 $list .= "$field = ";
275 }
276 $list .= 'NULL';
277 } else {
278 if (
279 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
280 ) {
281 $list .= "$field = ";
282 }
283 $list .= $mode == self::LIST_NAMES ? $value : $this->quoter->addQuotes( $value );
284 }
285 }
286
287 return $list;
288 }
289
290 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
291 $conds = [];
292 foreach ( $data as $base => $sub ) {
293 if ( count( $sub ) ) {
294 $conds[] = $this->makeList(
295 [ $baseKey => $base, $subKey => array_map( 'strval', array_keys( $sub ) ) ],
296 self::LIST_AND
297 );
298 }
299 }
300
301 if ( !$conds ) {
302 throw new InvalidArgumentException( "Data for $baseKey and $subKey must be non-empty" );
303 }
304
305 return $this->makeList( $conds, self::LIST_OR );
306 }
307
308 public function factorConds( $condsArray ) {
309 if ( count( $condsArray ) === 0 ) {
310 throw new InvalidArgumentException(
311 __METHOD__ . ": empty condition array" );
312 }
313 $condsByFieldSet = [];
314 foreach ( $condsArray as $conds ) {
315 if ( !count( $conds ) ) {
316 throw new InvalidArgumentException(
317 __METHOD__ . ": empty condition subarray" );
318 }
319 $fieldKey = implode( ',', array_keys( $conds ) );
320 $condsByFieldSet[$fieldKey][] = $conds;
321 }
322 $result = '';
323 foreach ( $condsByFieldSet as $conds ) {
324 if ( $result !== '' ) {
325 $result .= ' OR ';
326 }
327 $result .= $this->factorCondsWithCommonFields( $conds );
328 }
329 return $result;
330 }
331
339 private function factorCondsWithCommonFields( $condsArray ) {
340 $first = $condsArray[array_key_first( $condsArray )];
341 if ( count( $first ) === 1 ) {
342 // IN clause
343 $field = array_key_first( $first );
344 $values = [];
345 foreach ( $condsArray as $conds ) {
346 $values[] = $conds[$field];
347 }
348 return $this->makeList( [ $field => $values ], self::LIST_AND );
349 }
350
351 $field1 = array_key_first( $first );
352 $nullExpressions = [];
353 $expressionsByField1 = [];
354 foreach ( $condsArray as $conds ) {
355 $value1 = $conds[$field1];
356 unset( $conds[$field1] );
357 if ( $value1 === null ) {
358 $nullExpressions[] = $conds;
359 } else {
360 $expressionsByField1[$value1][] = $conds;
361 }
362
363 }
364 $wrap = false;
365 $result = '';
366 foreach ( $expressionsByField1 as $value1 => $expressions ) {
367 if ( $result !== '' ) {
368 $result .= ' OR ';
369 $wrap = true;
370 }
371 $factored = $this->factorCondsWithCommonFields( $expressions );
372 $result .= "($field1 = " . $this->quoter->addQuotes( $value1 ) .
373 " AND $factored)";
374 }
375 if ( count( $nullExpressions ) ) {
376 $factored = $this->factorCondsWithCommonFields( $nullExpressions );
377 if ( $result !== '' ) {
378 $result .= ' OR ';
379 $wrap = true;
380 }
381 $result .= "($field1 IS NULL AND $factored)";
382 }
383 if ( $wrap ) {
384 return "($result)";
385 } else {
386 return $result;
387 }
388 }
389
394 public function buildConcat( $stringList ) {
395 return 'CONCAT(' . implode( ',', $stringList ) . ')';
396 }
397
398 public function limitResult( $sql, $limit, $offset = false ) {
399 if ( !is_numeric( $limit ) ) {
400 throw new DBLanguageError(
401 "Invalid non-numeric limit passed to " . __METHOD__
402 );
403 }
404 // This version works in MySQL and SQLite. It will very likely need to be
405 // overridden for most other RDBMS subclasses.
406 return "$sql LIMIT "
407 . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
408 . "{$limit} ";
409 }
410
417 public function escapeLikeInternal( $s, $escapeChar = '`' ) {
418 return str_replace(
419 [ $escapeChar, '%', '_' ],
420 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
421 $s
422 );
423 }
424
425 public function buildLike( $param, ...$params ) {
426 if ( is_array( $param ) ) {
427 $params = $param;
428 } else {
429 $params = func_get_args();
430 }
431 // @phan-suppress-next-line PhanParamTooFewUnpack
432 $likeValue = new LikeValue( ...$params );
433
434 return ' LIKE ' . $likeValue->toSql( $this->quoter );
435 }
436
437 public function anyChar() {
438 return new LikeMatch( '_' );
439 }
440
441 public function anyString() {
442 return new LikeMatch( '%' );
443 }
444
449 public function unionSupportsOrderAndLimit() {
450 return true; // True for almost every DB supported
451 }
452
453 public function unionQueries( $sqls, $all, $options = [] ) {
454 $glue = $all ? ') UNION ALL (' : ') UNION (';
455
456 $sql = '(' . implode( $glue, $sqls ) . ')';
457 if ( !$this->unionSupportsOrderAndLimit() ) {
458 return $sql;
459 }
460 $sql .= $this->makeOrderBy( $options );
461 $limit = $options['LIMIT'] ?? null;
462 $offset = $options['OFFSET'] ?? false;
463 if ( $limit !== null ) {
464 $sql = $this->limitResult( $sql, $limit, $offset );
465 }
466
467 return $sql;
468 }
469
470 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
471 if ( is_array( $cond ) ) {
472 $cond = $this->makeList( $cond, self::LIST_AND );
473 }
474 if ( $cond instanceof IExpression ) {
475 $cond = $cond->toSql( $this->quoter );
476 }
477
478 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
479 }
480
481 public function strreplace( $orig, $old, $new ) {
482 return "REPLACE({$orig}, {$old}, {$new})";
483 }
484
485 public function timestamp( $ts = 0 ) {
486 $t = new ConvertibleTimestamp( $ts );
487 // Let errors bubble up to avoid putting garbage in the DB
488 return $t->getTimestamp( TS_MW );
489 }
490
491 public function timestampOrNull( $ts = null ) {
492 if ( $ts === null ) {
493 return null;
494 } else {
495 return $this->timestamp( $ts );
496 }
497 }
498
499 public function getInfinity() {
500 return 'infinity';
501 }
502
503 public function encodeExpiry( $expiry ) {
504 return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
505 ? $this->getInfinity()
506 : $this->timestamp( $expiry );
507 }
508
509 public function decodeExpiry( $expiry, $format = TS_MW ) {
510 if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
511 return 'infinity';
512 }
513
514 return ConvertibleTimestamp::convert( $format, $expiry );
515 }
516
521 public function buildSubstring( $input, $startPosition, $length = null ) {
522 $this->assertBuildSubstringParams( $startPosition, $length );
523 $functionBody = "$input FROM $startPosition";
524 if ( $length !== null ) {
525 $functionBody .= " FOR $length";
526 }
527 return 'SUBSTRING(' . $functionBody . ')';
528 }
529
542 protected function assertBuildSubstringParams( $startPosition, $length ) {
543 if ( $startPosition === 0 ) {
544 // The DBMSs we support use 1-based indexing here.
545 throw new InvalidArgumentException( 'Use 1 as $startPosition for the beginning of the string' );
546 }
547 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
548 throw new InvalidArgumentException(
549 '$startPosition must be a positive integer'
550 );
551 }
552 if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
553 throw new InvalidArgumentException(
554 '$length must be null or an integer greater than or equal to 0'
555 );
556 }
557 }
558
559 public function buildStringCast( $field ) {
560 // In theory this should work for any standards-compliant
561 // SQL implementation, although it may not be the best way to do it.
562 return "CAST( $field AS CHARACTER )";
563 }
564
565 public function buildIntegerCast( $field ) {
566 return 'CAST( ' . $field . ' AS INTEGER )';
567 }
568
569 public function implicitOrderby() {
570 return true;
571 }
572
581 public function indexName( $index ) {
582 return $this->indexAliases[$index] ?? $index;
583 }
584
585 public function setTableAliases( array $aliases ) {
586 $this->tableAliases = $aliases;
587 }
588
589 public function setIndexAliases( array $aliases ) {
590 $this->indexAliases = $aliases;
591 }
592
596 public function getTableAliases() {
597 return $this->tableAliases;
598 }
599
600 public function setPrefix( $prefix ) {
601 $this->currentDomain = new DatabaseDomain(
602 $this->currentDomain->getDatabase(),
603 $this->currentDomain->getSchema(),
604 $prefix
605 );
606 }
607
608 public function setCurrentDomain( DatabaseDomain $currentDomain ) {
609 $this->currentDomain = $currentDomain;
610 }
611
612 public function selectSQLText(
613 $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
614 ) {
615 if ( is_array( $table ) ) {
616 $tables = $table;
617 } elseif ( $table === '' || $table === null || $table === false ) {
618 $tables = [];
619 } elseif ( is_string( $table ) ) {
620 $tables = [ $table ];
621 } else {
622 throw new DBLanguageError( __METHOD__ . ' called with incorrect table parameter' );
623 }
624
625 if ( is_array( $vars ) ) {
626 $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
627 } else {
628 $fields = $vars;
629 }
630
631 $options = (array)$options;
632
633 $useIndexByTable = $options['USE INDEX'] ?? [];
634 if ( !is_array( $useIndexByTable ) ) {
635 if ( count( $tables ) <= 1 ) {
636 $useIndexByTable = [ reset( $tables ) => $useIndexByTable ];
637 } else {
638 $e = new DBLanguageError( __METHOD__ . " got ambiguous USE INDEX ($fname)" );
639 ( $this->errorLogger )( $e );
640 }
641 }
642
643 $ignoreIndexByTable = $options['IGNORE INDEX'] ?? [];
644 if ( !is_array( $ignoreIndexByTable ) ) {
645 if ( count( $tables ) <= 1 ) {
646 $ignoreIndexByTable = [ reset( $tables ) => $ignoreIndexByTable ];
647 } else {
648 $e = new DBLanguageError( __METHOD__ . " got ambiguous IGNORE INDEX ($fname)" );
649 ( $this->errorLogger )( $e );
650 }
651 }
652
653 if (
654 $this->selectOptionsIncludeLocking( $options ) &&
655 $this->selectFieldsOrOptionsAggregate( $vars, $options )
656 ) {
657 // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
658 // functions. Discourage use of such queries to encourage compatibility.
659 $this->logger->warning(
660 __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
661 );
662 }
663
664 if ( count( $tables ) ) {
665 $from = ' FROM ' . $this->tableNamesWithIndexClauseOrJOIN(
666 $tables,
667 $useIndexByTable,
668 $ignoreIndexByTable,
669 $join_conds
670 );
671 } else {
672 $from = '';
673 }
674
675 [ $startOpts, $preLimitTail, $postLimitTail ] = $this->makeSelectOptions( $options );
676
677 if ( is_array( $conds ) ) {
678 $where = $this->makeList( $conds, self::LIST_AND );
679 } elseif ( $conds instanceof IExpression ) {
680 $where = $conds->toSql( $this->quoter );
681 } elseif ( $conds === null || $conds === false ) {
682 $where = '';
683 $this->logger->warning(
684 __METHOD__
685 . ' called from '
686 . $fname
687 . ' with incorrect parameters: $conds must be a string or an array',
688 [ 'db_log_category' => 'sql' ]
689 );
690 } elseif ( is_string( $conds ) ) {
691 $where = $conds;
692 } else {
693 throw new DBLanguageError( __METHOD__ . ' called with incorrect parameters' );
694 }
695
696 // Keep historical extra spaces after FROM to avoid testing failures
697 if ( $where === '' || $where === '*' ) {
698 $sql = "SELECT $startOpts $fields $from $preLimitTail";
699 } else {
700 $sql = "SELECT $startOpts $fields $from WHERE $where $preLimitTail";
701 }
702
703 if ( isset( $options['LIMIT'] ) ) {
704 $sql = $this->limitResult( $sql, $options['LIMIT'], $options['OFFSET'] ?? false );
705 }
706 $sql = "$sql $postLimitTail";
707
708 if ( isset( $options['EXPLAIN'] ) ) {
709 $sql = 'EXPLAIN ' . $sql;
710 }
711
712 return $sql;
713 }
714
719 private function selectOptionsIncludeLocking( $options ) {
720 $options = (array)$options;
721 foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
722 if ( in_array( $lock, $options, true ) ) {
723 return true;
724 }
725 }
726
727 return false;
728 }
729
735 private function selectFieldsOrOptionsAggregate( $fields, $options ) {
736 foreach ( (array)$options as $key => $value ) {
737 if ( is_string( $key ) ) {
738 if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
739 return true;
740 }
741 } elseif ( is_string( $value ) ) {
742 if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
743 return true;
744 }
745 }
746 }
747
748 $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
749 foreach ( (array)$fields as $field ) {
750 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
751 return true;
752 }
753 }
754
755 return false;
756 }
757
764 protected function fieldNamesWithAlias( $fields ) {
765 $retval = [];
766 foreach ( $fields as $alias => $field ) {
767 if ( is_numeric( $alias ) ) {
768 $alias = $field;
769 }
770 $retval[] = $this->fieldNameWithAlias( $field, $alias );
771 }
772
773 return $retval;
774 }
775
785 public function fieldNameWithAlias( $name, $alias = false ) {
786 if ( !$alias || (string)$alias === (string)$name ) {
787 return $name;
788 } else {
789 return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
790 }
791 }
792
804 $tables,
805 $use_index = [],
806 $ignore_index = [],
807 $join_conds = []
808 ) {
809 $ret = [];
810 $retJOIN = [];
811 $use_index = (array)$use_index;
812 $ignore_index = (array)$ignore_index;
813 $join_conds = (array)$join_conds;
814
815 foreach ( $tables as $alias => $table ) {
816 if ( !is_string( $alias ) ) {
817 // No alias? Set it equal to the table name
818 $alias = $table;
819 }
820
821 if ( is_array( $table ) ) {
822 // A parenthesized group
823 if ( count( $table ) > 1 ) {
824 $joinedTable = '(' .
825 $this->tableNamesWithIndexClauseOrJOIN(
826 $table, $use_index, $ignore_index, $join_conds ) . ')';
827 } else {
828 // Degenerate case
829 $innerTable = reset( $table );
830 $innerAlias = key( $table );
831 $joinedTable = $this->tableNameWithAlias(
832 $innerTable,
833 is_string( $innerAlias ) ? $innerAlias : $innerTable
834 );
835 }
836 } else {
837 $joinedTable = $this->tableNameWithAlias( $table, $alias );
838 }
839
840 // Is there a JOIN clause for this table?
841 if ( isset( $join_conds[$alias] ) ) {
842 Assert::parameterType( 'array', $join_conds[$alias], "join_conds[$alias]" );
843 [ $joinType, $conds ] = $join_conds[$alias];
844 $tableClause = $this->normalizeJoinType( $joinType );
845 $tableClause .= ' ' . $joinedTable;
846 if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
847 $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
848 if ( $use != '' ) {
849 $tableClause .= ' ' . $use;
850 }
851 }
852 if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
853 $ignore = $this->ignoreIndexClause(
854 implode( ',', (array)$ignore_index[$alias] ) );
855 if ( $ignore != '' ) {
856 $tableClause .= ' ' . $ignore;
857 }
858 }
859 $on = $this->makeList( (array)$conds, self::LIST_AND );
860 if ( $on != '' ) {
861 $tableClause .= ' ON (' . $on . ')';
862 }
863
864 $retJOIN[] = $tableClause;
865 } elseif ( isset( $use_index[$alias] ) ) {
866 // Is there an INDEX clause for this table?
867 $tableClause = $joinedTable;
868 $tableClause .= ' ' . $this->useIndexClause(
869 implode( ',', (array)$use_index[$alias] )
870 );
871
872 $ret[] = $tableClause;
873 } elseif ( isset( $ignore_index[$alias] ) ) {
874 // Is there an INDEX clause for this table?
875 $tableClause = $joinedTable;
876 $tableClause .= ' ' . $this->ignoreIndexClause(
877 implode( ',', (array)$ignore_index[$alias] )
878 );
879
880 $ret[] = $tableClause;
881 } else {
882 $tableClause = $joinedTable;
883
884 $ret[] = $tableClause;
885 }
886 }
887
888 // We can't separate explicit JOIN clauses with ',', use ' ' for those
889 $implicitJoins = implode( ',', $ret );
890 $explicitJoins = implode( ' ', $retJOIN );
891
892 // Compile our final table clause
893 return implode( ' ', [ $implicitJoins, $explicitJoins ] );
894 }
895
904 protected function normalizeJoinType( string $joinType ) {
905 switch ( strtoupper( $joinType ) ) {
906 case 'JOIN':
907 case 'INNER JOIN':
908 return 'JOIN';
909
910 case 'LEFT JOIN':
911 return 'LEFT JOIN';
912
913 case 'STRAIGHT_JOIN':
914 case 'STRAIGHT JOIN':
915 // MySQL only
916 return 'JOIN';
917
918 default:
919 return $joinType;
920 }
921 }
922
934 protected function tableNameWithAlias( $table, $alias = false ) {
935 if ( is_string( $table ) ) {
936 $quotedTable = $this->tableName( $table );
937 } elseif ( $table instanceof Subquery ) {
938 $quotedTable = (string)$table;
939 } else {
940 throw new InvalidArgumentException( "Table must be a string or Subquery" );
941 }
942
943 if ( $alias === false || $alias === $table ) {
944 if ( $table instanceof Subquery ) {
945 throw new InvalidArgumentException( "Subquery table missing alias" );
946 }
947
948 return $quotedTable;
949 } else {
950 return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
951 }
952 }
953
954 public function tableName( $name, $format = 'quoted' ) {
955 if ( $name instanceof Subquery ) {
956 throw new DBLanguageError(
957 __METHOD__ . ': got Subquery instance when expecting a string'
958 );
959 }
960
961 # Lets test for any bits of text that should never show up in a table
962 # name. Basically anything like JOIN or ON which are actually part of
963 # SQL queries, but may end up inside of the table value to combine SQL.
964 # Note that we use a whitespace test rather than a \b test to avoid
965 # any remote case where a word like ON may be inside of a table name
966 # surrounded by symbols which may be considered word breaks.
967 if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
968 throw new DBLanguageError(
969 __METHOD__ . ": use of subqueries is not supported this way"
970 );
971 }
972
973 # Extract necessary database, schema, table identifiers and quote them as needed
974 $formattedComponents = [];
975 foreach ( $this->qualifiedTableComponents( $name ) as $component ) {
976 if ( $format === 'quoted' ) {
977 $formattedComponents[] = $this->addIdentifierQuotes( $component );
978 } else {
979 $formattedComponents[] = $component;
980 }
981 }
982
983 return implode( '.', $formattedComponents );
984 }
985
1008 public function qualifiedTableComponents( $name ) {
1009 $identifiers = $this->extractTableNameComponents( $name );
1010 if ( count( $identifiers ) > 3 ) {
1011 throw new DBLanguageError( "Too many components in table name '$name'" );
1012 }
1013 // Table alias config and prefixes only apply to unquoted single-identifier names
1014 if ( count( $identifiers ) == 1 && !$this->isQuotedIdentifier( $identifiers[0] ) ) {
1015 [ $table ] = $identifiers;
1016 if ( isset( $this->tableAliases[$table] ) ) {
1017 // This is an "alias" table that uses a different db/schema/prefix scheme
1018 $database = $this->tableAliases[$table]['dbname'];
1019 $schema = is_string( $this->tableAliases[$table]['schema'] )
1020 ? $this->tableAliases[$table]['schema']
1021 : $this->relationSchemaQualifier();
1022 $prefix = is_string( $this->tableAliases[$table]['prefix'] )
1023 ? $this->tableAliases[$table]['prefix']
1024 : $this->currentDomain->getTablePrefix();
1025 } else {
1026 // Use the current database domain to resolve the schema and prefix
1027 $database = '';
1028 $schema = $this->relationSchemaQualifier();
1029 $prefix = $this->currentDomain->getTablePrefix();
1030 }
1031 $qualifierIdentifiers = [ $database, $schema ];
1032 $tableIdentifier = $prefix . $table;
1033 } else {
1034 $qualifierIdentifiers = array_slice( $identifiers, 0, -1 );
1035 $tableIdentifier = end( $identifiers );
1036 }
1037
1038 $components = [];
1039 foreach ( $qualifierIdentifiers as $identifier ) {
1040 if ( $identifier !== null && $identifier !== '' ) {
1041 $components[] = $this->isQuotedIdentifier( $identifier )
1042 ? substr( $identifier, 1, -1 )
1043 : $identifier;
1044 }
1045 }
1046 $components[] = $this->isQuotedIdentifier( $tableIdentifier )
1047 ? substr( $tableIdentifier, 1, -1 )
1048 : $tableIdentifier;
1049
1050 return $components;
1051 }
1052
1059 public function extractTableNameComponents( string $name ) {
1060 $quoteChar = $this->getIdentifierQuoteChar();
1061 $components = [];
1062 foreach ( explode( '.', $name ) as $component ) {
1063 if ( $this->isQuotedIdentifier( $component ) ) {
1064 $unquotedComponent = substr( $component, 1, -1 );
1065 } else {
1066 $unquotedComponent = $component;
1067 }
1068 if ( str_contains( $unquotedComponent, $quoteChar ) ) {
1069 throw new DBLanguageError(
1070 'Table name component contains unexpected quote or dot character' );
1071 }
1072 $components[] = $component;
1073 }
1074 return $components;
1075 }
1076
1081 protected function relationSchemaQualifier() {
1082 return $this->currentDomain->getSchema();
1083 }
1084
1085 public function tableNames( ...$tables ) {
1086 $retVal = [];
1087
1088 foreach ( $tables as $name ) {
1089 $retVal[$name] = $this->tableName( $name );
1090 }
1091
1092 return $retVal;
1093 }
1094
1095 public function tableNamesN( ...$tables ) {
1096 $retVal = [];
1097
1098 foreach ( $tables as $name ) {
1099 $retVal[] = $this->tableName( $name );
1100 }
1101
1102 return $retVal;
1103 }
1104
1114 public function isQuotedIdentifier( $name ) {
1115 $quoteChar = $this->getIdentifierQuoteChar();
1116 return strlen( $name ) > 1 && $name[0] === $quoteChar && $name[-1] === $quoteChar;
1117 }
1118
1132 public function useIndexClause( $index ) {
1133 return '';
1134 }
1135
1144 public function ignoreIndexClause( $index ) {
1145 return '';
1146 }
1147
1159 protected function makeSelectOptions( array $options ) {
1160 $preLimitTail = $postLimitTail = '';
1161 $startOpts = '';
1162
1163 $noKeyOptions = [];
1164
1165 foreach ( $options as $key => $option ) {
1166 if ( is_numeric( $key ) ) {
1167 $noKeyOptions[$option] = true;
1168 }
1169 }
1170
1171 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1172
1173 $preLimitTail .= $this->makeOrderBy( $options );
1174
1175 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1176 $postLimitTail .= ' FOR UPDATE';
1177 }
1178
1179 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1180 $postLimitTail .= ' LOCK IN SHARE MODE';
1181 }
1182
1183 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1184 $startOpts .= 'DISTINCT';
1185 }
1186
1187 # Various MySQL extensions
1188 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1189 $startOpts .= ' /*! STRAIGHT_JOIN */';
1190 }
1191
1192 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1193 $startOpts .= ' SQL_BIG_RESULT';
1194 }
1195
1196 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1197 $startOpts .= ' SQL_BUFFER_RESULT';
1198 }
1199
1200 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1201 $startOpts .= ' SQL_SMALL_RESULT';
1202 }
1203
1204 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1205 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1206 }
1207
1208 return [ $startOpts, $preLimitTail, $postLimitTail ];
1209 }
1210
1219 protected function makeGroupByWithHaving( $options ) {
1220 $sql = '';
1221 if ( isset( $options['GROUP BY'] ) ) {
1222 $gb = is_array( $options['GROUP BY'] )
1223 ? implode( ',', $options['GROUP BY'] )
1224 : $options['GROUP BY'];
1225 $sql .= ' GROUP BY ' . $gb;
1226 }
1227 if ( isset( $options['HAVING'] ) ) {
1228 $having = is_array( $options['HAVING'] )
1229 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1230 : $options['HAVING'];
1231 $sql .= ' HAVING ' . $having;
1232 }
1233
1234 return $sql;
1235 }
1236
1245 protected function makeOrderBy( $options ) {
1246 if ( isset( $options['ORDER BY'] ) ) {
1247 $ob = is_array( $options['ORDER BY'] )
1248 ? implode( ',', $options['ORDER BY'] )
1249 : $options['ORDER BY'];
1250
1251 return ' ORDER BY ' . $ob;
1252 }
1253
1254 return '';
1255 }
1256
1257 public function buildGroupConcatField(
1258 $delim, $table, $field, $conds = '', $join_conds = []
1259 ) {
1260 $fld = "GROUP_CONCAT($field SEPARATOR " . $this->quoter->addQuotes( $delim ) . ')';
1261
1262 return '(' . $this->selectSQLText( $table, $fld, $conds, __METHOD__, [], $join_conds ) . ')';
1263 }
1264
1265 public function buildSelectSubquery(
1266 $table, $vars, $conds = '', $fname = __METHOD__,
1267 $options = [], $join_conds = []
1268 ) {
1269 return new Subquery(
1270 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
1271 );
1272 }
1273
1274 public function insertSqlText( $table, array $rows ) {
1275 $encTable = $this->tableName( $table );
1276 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1277
1278 return [
1279 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1280 "INSERT INTO $encTable ($sqlColumns) VALUES '?'"
1281 ];
1282 }
1283
1296 public function makeInsertLists( array $rows, $aliasPrefix = '', array $typeByColumn = [] ) {
1297 $firstRow = $rows[0];
1298 if ( !is_array( $firstRow ) || !$firstRow ) {
1299 throw new DBLanguageError( 'Got an empty row list or empty row' );
1300 }
1301 // List of columns that define the value tuple ordering
1302 $tupleColumns = array_keys( $firstRow );
1303
1304 $valueTuples = [];
1305 foreach ( $rows as $row ) {
1306 $rowColumns = array_keys( $row );
1307 // VALUES(...) requires a uniform correspondence of (column => value)
1308 if ( $rowColumns !== $tupleColumns ) {
1309 throw new DBLanguageError(
1310 'Got row columns (' . implode( ', ', $rowColumns ) . ') ' .
1311 'instead of expected (' . implode( ', ', $tupleColumns ) . ')'
1312 );
1313 }
1314 // Make the value tuple that defines this row
1315 $valueTuples[] = '(' . $this->makeList( $row, self::LIST_COMMA ) . ')';
1316 }
1317
1318 $magicAliasFields = [];
1319 foreach ( $tupleColumns as $column ) {
1320 $magicAliasFields[] = $aliasPrefix . $column;
1321 }
1322
1323 return [
1324 $this->makeList( $tupleColumns, self::LIST_NAMES ),
1325 implode( ',', $valueTuples ),
1326 $this->makeList( $magicAliasFields, self::LIST_NAMES )
1327 ];
1328 }
1329
1330 public function insertNonConflictingSqlText( $table, array $rows ) {
1331 $encTable = $this->tableName( $table );
1332 [ $sqlColumns, $sqlTuples ] = $this->makeInsertLists( $rows );
1333 [ $sqlVerb, $sqlOpts ] = $this->makeInsertNonConflictingVerbAndOptions();
1334
1335 return [
1336 rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" ),
1337 rtrim( "$sqlVerb $encTable ($sqlColumns) VALUES '?' $sqlOpts" )
1338 ];
1339 }
1340
1347 return [ 'INSERT IGNORE INTO', '' ];
1348 }
1349
1351 $destTable,
1352 $srcTable,
1353 array $varMap,
1354 $conds,
1355 $fname,
1356 array $insertOptions,
1357 array $selectOptions,
1358 $selectJoinConds
1359 ) {
1360 [ $sqlVerb, $sqlOpts ] = $this->isFlagInOptions( 'IGNORE', $insertOptions )
1361 ? $this->makeInsertNonConflictingVerbAndOptions()
1362 : [ 'INSERT INTO', '' ];
1363 $encDstTable = $this->tableName( $destTable );
1364 $sqlDstColumns = implode( ',', array_keys( $varMap ) );
1365 $selectSql = $this->selectSQLText(
1366 $srcTable,
1367 array_values( $varMap ),
1368 $conds,
1369 $fname,
1370 $selectOptions,
1371 $selectJoinConds
1372 );
1373
1374 return rtrim( "$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
1375 }
1376
1383 public function isFlagInOptions( $option, array $options ) {
1384 foreach ( array_keys( $options, $option, true ) as $k ) {
1385 if ( is_int( $k ) ) {
1386 return true;
1387 }
1388 }
1389
1390 return false;
1391 }
1392
1400 public function makeKeyCollisionCondition( array $rows, array $uniqueKey ) {
1401 if ( !$rows ) {
1402 throw new DBLanguageError( "Empty row array" );
1403 } elseif ( !$uniqueKey ) {
1404 throw new DBLanguageError( "Empty unique key array" );
1405 }
1406
1407 if ( count( $uniqueKey ) == 1 ) {
1408 // Use a simple IN(...) clause
1409 $column = reset( $uniqueKey );
1410 $values = array_column( $rows, $column );
1411 if ( count( $values ) !== count( $rows ) ) {
1412 throw new DBLanguageError( "Missing values for unique key ($column)" );
1413 }
1414
1415 return $this->makeList( [ $column => $values ], self::LIST_AND );
1416 }
1417
1418 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey, null );
1419
1420 $orConds = [];
1421 foreach ( $rows as $row ) {
1422 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
1423 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
1424 throw new DBLanguageError(
1425 "Missing values for unique key (" . implode( ',', $uniqueKey ) . ")"
1426 );
1427 }
1428 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
1429 }
1430
1431 return count( $orConds ) > 1
1432 ? $this->makeList( $orConds, self::LIST_OR )
1433 : $orConds[0];
1434 }
1435
1436 public function deleteJoinSqlText( $delTable, $joinTable, $delVar, $joinVar, $conds ) {
1437 if ( !$conds ) {
1438 throw new DBLanguageError( __METHOD__ . ' called with empty $conds' );
1439 }
1440
1441 $delTable = $this->tableName( $delTable );
1442 $joinTable = $this->tableName( $joinTable );
1443 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
1444 if ( $conds != '*' ) {
1445 $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
1446 }
1447 $sql .= ')';
1448
1449 return $sql;
1450 }
1451
1457 public function deleteSqlText( $table, $conds ) {
1458 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1459 if ( !$isCondValid ) {
1460 throw new DBLanguageError( __METHOD__ . ' called with empty conditions' );
1461 }
1462
1463 $encTable = $this->tableName( $table );
1464 $sql = "DELETE FROM $encTable";
1465
1466 $condsSql = '';
1467 $cleanCondsSql = '';
1468 if ( $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1469 $cleanCondsSql = ' WHERE ' . $this->scrubArray( $conds );
1470 if ( is_array( $conds ) ) {
1471 $conds = $this->makeList( $conds, self::LIST_AND );
1472 }
1473 $condsSql .= ' WHERE ' . $conds;
1474 }
1475 return new Query(
1476 $sql . $condsSql,
1477 self::QUERY_CHANGE_ROWS,
1478 'DELETE',
1479 $table,
1480 $sql . $cleanCondsSql
1481 );
1482 }
1483
1484 private function scrubArray( $array, $listType = self::LIST_AND ) {
1485 if ( is_array( $array ) ) {
1486 $scrubbedArray = [];
1487 foreach ( $array as $key => $value ) {
1488 if ( $value instanceof IExpression ) {
1489 $scrubbedArray[$key] = $value->toGeneralizedSql();
1490 } else {
1491 $scrubbedArray[$key] = '?';
1492 }
1493 }
1494 return $this->makeList( $scrubbedArray, $listType );
1495 }
1496 return '?';
1497 }
1498
1499 public function updateSqlText( $table, $set, $conds, $options ) {
1500 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
1501 if ( !$isCondValid ) {
1502 throw new DBLanguageError( __METHOD__ . ' called with empty conditions' );
1503 }
1504 $encTable = $this->tableName( $table );
1505 $opts = $this->makeUpdateOptions( $options );
1506 $sql = "UPDATE $opts $encTable";
1507 $condsSql = " SET " . $this->makeList( $set, self::LIST_SET );
1508 $cleanCondsSql = " SET " . $this->scrubArray( $set, self::LIST_SET );
1509
1510 if ( $conds && $conds !== self::ALL_ROWS && $conds !== [ self::ALL_ROWS ] ) {
1511 $cleanCondsSql .= ' WHERE ' . $this->scrubArray( $conds );
1512 if ( is_array( $conds ) ) {
1513 $conds = $this->makeList( $conds, self::LIST_AND );
1514 }
1515 $condsSql .= ' WHERE ' . $conds;
1516 }
1517 return new Query(
1518 $sql . $condsSql,
1519 self::QUERY_CHANGE_ROWS,
1520 'UPDATE',
1521 $table,
1522 $sql . $cleanCondsSql
1523 );
1524 }
1525
1532 protected function makeUpdateOptions( $options ) {
1533 $opts = $this->makeUpdateOptionsArray( $options );
1534
1535 return implode( ' ', $opts );
1536 }
1537
1545 protected function makeUpdateOptionsArray( $options ) {
1546 $options = $this->normalizeOptions( $options );
1547
1548 $opts = [];
1549
1550 if ( in_array( 'IGNORE', $options ) ) {
1551 $opts[] = 'IGNORE';
1552 }
1553
1554 return $opts;
1555 }
1556
1562 final public function normalizeOptions( $options ) {
1563 if ( is_array( $options ) ) {
1564 return $options;
1565 } elseif ( is_string( $options ) ) {
1566 return ( $options === '' ) ? [] : [ $options ];
1567 } else {
1568 throw new DBLanguageError( __METHOD__ . ': expected string or array' );
1569 }
1570 }
1571
1572 public function dropTableSqlText( $table ) {
1573 // https://mariadb.com/kb/en/drop-table/
1574 // https://dev.mysql.com/doc/refman/8.0/en/drop-table.html
1575 // https://www.postgresql.org/docs/9.2/sql-truncate.html
1576 return "DROP TABLE " . $this->tableName( $table ) . " CASCADE";
1577 }
1578
1583 public function getQueryVerb( $sql ) {
1584 return QueryBuilderFromRawSql::buildQuery( $sql, 0 )->getVerb();
1585 }
1586
1599 public function isTransactableQuery( Query $sql ) {
1600 return !in_array(
1601 $sql->getVerb(),
1602 [
1603 'BEGIN',
1604 'ROLLBACK',
1605 'ROLLBACK TO SAVEPOINT',
1606 'COMMIT',
1607 'SET',
1608 'SHOW',
1609 'CREATE',
1610 'ALTER',
1611 'USE',
1612 'SHOW'
1613 ],
1614 true
1615 );
1616 }
1617
1636 public function isWriteQuery( $sql, $flags ) {
1637 return QueryBuilderFromRawSql::buildQuery( $sql, $flags )->isWriteQuery();
1638 }
1639
1640 public function buildExcludedValue( $column ) {
1641 /* @see Database::upsert() */
1642 // This can be treated like a single value since __VALS is a single row table
1643 return "(SELECT __$column FROM __VALS)";
1644 }
1645
1646 public function savepointSqlText( $identifier ) {
1647 return 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1648 }
1649
1650 public function releaseSavepointSqlText( $identifier ) {
1651 return 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1652 }
1653
1654 public function rollbackToSavepointSqlText( $identifier ) {
1655 return 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
1656 }
1657
1658 public function rollbackSqlText() {
1659 return 'ROLLBACK';
1660 }
1661
1662 public function dispatchingInsertSqlText( $table, $rows, $options ) {
1663 $rows = $this->normalizeRowArray( $rows );
1664 if ( !$rows ) {
1665 return false;
1666 }
1667
1668 $options = $this->normalizeOptions( $options );
1669 if ( $this->isFlagInOptions( 'IGNORE', $options ) ) {
1670 [ $sql, $cleanSql ] = $this->insertNonConflictingSqlText( $table, $rows );
1671 } else {
1672 [ $sql, $cleanSql ] = $this->insertSqlText( $table, $rows );
1673 }
1674 return new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $table, $cleanSql );
1675 }
1676
1682 final protected function normalizeRowArray( array $rowOrRows ) {
1683 if ( !$rowOrRows ) {
1684 $rows = [];
1685 } elseif ( isset( $rowOrRows[0] ) ) {
1686 $rows = $rowOrRows;
1687 } else {
1688 $rows = [ $rowOrRows ];
1689 }
1690
1691 foreach ( $rows as $row ) {
1692 if ( !is_array( $row ) ) {
1693 throw new DBLanguageError( "Got non-array in row array" );
1694 } elseif ( !$row ) {
1695 throw new DBLanguageError( "Got empty array in row array" );
1696 }
1697 }
1698
1699 return $rows;
1700 }
1701
1710 final public function normalizeUpsertParams( $uniqueKeys, &$rows ) {
1711 $rows = $this->normalizeRowArray( $rows );
1712 if ( !$uniqueKeys ) {
1713 throw new DBLanguageError( 'No unique key specified for upsert/replace' );
1714 }
1715 $uniqueKey = $this->normalizeUpsertKeys( $uniqueKeys );
1716 $this->assertValidUpsertRowArray( $rows, $uniqueKey );
1717
1718 return $uniqueKey;
1719 }
1720
1727 final public function normalizeConditions( $conds, $fname ) {
1728 if ( $conds === null || $conds === false ) {
1729 $this->logger->warning(
1730 __METHOD__
1731 . ' called from '
1732 . $fname
1733 . ' with incorrect parameters: $conds must be a string or an array',
1734 [ 'db_log_category' => 'sql' ]
1735 );
1736 return [];
1737 } elseif ( $conds === '' ) {
1738 return [];
1739 }
1740
1741 return is_array( $conds ) ? $conds : [ $conds ];
1742 }
1743
1749 private function normalizeUpsertKeys( $uniqueKeys ) {
1750 if ( is_string( $uniqueKeys ) ) {
1751 return [ $uniqueKeys ];
1752 } elseif ( !is_array( $uniqueKeys ) ) {
1753 throw new DBLanguageError( 'Invalid unique key array' );
1754 } else {
1755 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
1756 throw new DBLanguageError(
1757 "The unique key array should contain a single unique index" );
1758 }
1759
1760 $uniqueKey = $uniqueKeys[0];
1761 if ( is_string( $uniqueKey ) ) {
1762 // Passing a list of strings for single-column unique keys is too
1763 // easily confused with passing the columns of composite unique key
1764 $this->logger->warning( __METHOD__ .
1765 " called with deprecated parameter style: " .
1766 "the unique key array should be a string or array of string arrays",
1767 [
1768 'exception' => new RuntimeException(),
1769 'db_log_category' => 'sql',
1770 ] );
1771 return $uniqueKeys;
1772 } elseif ( is_array( $uniqueKey ) ) {
1773 return $uniqueKey;
1774 } else {
1775 throw new DBLanguageError( 'Invalid unique key array entry' );
1776 }
1777 }
1778 }
1779
1785 final protected function assertValidUpsertRowArray( array $rows, array $uniqueKey ) {
1786 foreach ( $rows as $row ) {
1787 foreach ( $uniqueKey as $column ) {
1788 if ( !isset( $row[$column] ) ) {
1789 throw new DBLanguageError(
1790 "NULL/absent values for unique key (" . implode( ',', $uniqueKey ) . ")"
1791 );
1792 }
1793 }
1794 }
1795 }
1796
1803 final public function assertValidUpsertSetArray(
1804 array $set,
1805 array $uniqueKey,
1806 array $rows
1807 ) {
1808 if ( !$set ) {
1809 throw new DBLanguageError( "Update assignment list can't be empty for upsert" );
1810 }
1811
1812 // Sloppy callers might construct the SET array using the ROW array, leaving redundant
1813 // column definitions for unique key columns. Detect this for backwards compatibility.
1814 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) : null;
1815 // Disallow value changes for any columns in the unique key. This avoids additional
1816 // insertion order dependencies that are unwieldy and difficult to implement efficiently
1817 // in PostgreSQL.
1818 foreach ( $set as $k => $v ) {
1819 if ( is_string( $k ) ) {
1820 // Key is a column name and value is a literal (e.g. string, int, null, ...)
1821 if ( in_array( $k, $uniqueKey, true ) ) {
1822 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
1823 $this->logger->warning(
1824 __METHOD__ . " called with redundant assignment to column '$k'",
1825 [
1826 'exception' => new RuntimeException(),
1827 'db_log_category' => 'sql',
1828 ]
1829 );
1830 } else {
1831 throw new DBLanguageError(
1832 "Cannot reassign column '$k' since it belongs to the provided unique key"
1833 );
1834 }
1835 }
1836 } elseif ( preg_match( '/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
1837 // Value is of the form "<unquoted alphanumeric column> = <SQL expression>"
1838 if ( in_array( $m[1], $uniqueKey, true ) ) {
1839 throw new DBLanguageError(
1840 "Cannot reassign column '{$m[1]}' since it belongs to the provided unique key"
1841 );
1842 }
1843 }
1844 }
1845 }
1846
1851 final public function extractSingleFieldFromList( $var ) {
1852 if ( is_array( $var ) ) {
1853 if ( !$var ) {
1854 $column = null;
1855 } elseif ( count( $var ) == 1 ) {
1856 $column = $var[0] ?? reset( $var );
1857 } else {
1858 throw new DBLanguageError( __METHOD__ . ': got multiple columns' );
1859 }
1860 } else {
1861 $column = $var;
1862 }
1863
1864 return $column;
1865 }
1866
1867 public function setSchemaVars( $vars ) {
1868 $this->schemaVars = is_array( $vars ) ? $vars : null;
1869 }
1870
1877 protected function getSchemaVars() {
1878 return $this->schemaVars ?? $this->getDefaultSchemaVars();
1879 }
1880
1890 protected function getDefaultSchemaVars() {
1891 return [];
1892 }
1893
1914 public function replaceVars( $ins ) {
1915 $vars = $this->getSchemaVars();
1916 return preg_replace_callback(
1917 '!
1918 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
1919 \'\{\$ (\w+) }\' | # 3. addQuotes
1920 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
1921 /\*\$ (\w+) \*/ # 5. leave unencoded
1922 !x',
1923 function ( $m ) use ( $vars ) {
1924 // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
1925 // check for both nonexistent keys *and* the empty string.
1926 if ( isset( $m[1] ) && $m[1] !== '' ) {
1927 if ( $m[1] === 'i' ) {
1928 return $this->indexName( $m[2] );
1929 } else {
1930 return $this->tableName( $m[2] );
1931 }
1932 } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
1933 return $this->quoter->addQuotes( $vars[$m[3]] );
1934 } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
1935 return $this->addIdentifierQuotes( $vars[$m[4]] );
1936 } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
1937 return $vars[$m[5]];
1938 } else {
1939 return $m[0];
1940 }
1941 },
1942 $ins
1943 );
1944 }
1945
1946 public function lockSQLText( $lockName, $timeout ) {
1947 throw new RuntimeException( 'locking must be implemented in subclasses' );
1948 }
1949
1950 public function lockIsFreeSQLText( $lockName ) {
1951 throw new RuntimeException( 'locking must be implemented in subclasses' );
1952 }
1953
1954 public function unlockSQLText( $lockName ) {
1955 throw new RuntimeException( 'locking must be implemented in subclasses' );
1956 }
1957}
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
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...
__construct(DbQuoter $quoter, LoggerInterface $logger=null, DatabaseDomain $currentDomain=null, $errorLogger=null)
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
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.
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.
tableNames(... $tables)
Fetch a number of table names into an associative array.
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
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.
assertValidUpsertRowArray(array $rows, array $uniqueKey)
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
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)
isWriteQuery( $sql, $flags)
Determine whether a query writes to the DB.
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)
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
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.
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subquery.
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
Interface for query language.