Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.07% covered (warning)
81.07%
167 / 206
71.70% covered (warning)
71.70%
38 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
SelectQueryBuilder
81.07% covered (warning)
81.07%
167 / 206
71.70% covered (warning)
71.70%
38 / 53
160.85
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 queryInfo
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
8.02
 rawTables
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 merge
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 newSubquery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 from
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tables
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 fields
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 select
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 field
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 clearFields
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 where
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 andWhere
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 conds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 joinConds
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAutoAlias
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newJoinGroup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 limit
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 lockInShareMode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 forUpdate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 distinct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMaxExecutionTime
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 groupBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 having
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orderBy
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 mergeOption
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 useIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 ignoreIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndexHint
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
8.12
 explain
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 straightJoinOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 bigResult
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 bufferResult
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 smallResult
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 calcFoundRows
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 option
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 options
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 recency
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 caller
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getCaller
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fetchResultSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchField
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 fetchFieldValues
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 fetchRow
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchRowCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 estimateRowCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRowCountVar
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 buildGroupConcatField
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 getSQL
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getQueryInfo
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 acquireRowLocks
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use Wikimedia\Rdbms\Platform\ISQLPlatform;
6
7/**
8 * Build SELECT queries with a fluent interface.
9 *
10 * Each query builder object must be used for a single database query only,
11 * and not be reused afterwards. To run multiple similar queries, you can
12 * create a query builder to set up most of your query, which you can use
13 * as a "template" to clone. You can then modify the cloned object for
14 * each individual query.
15 *
16 * Note that the methods in this class are not stable to override.
17 * This class may be extended to create query builders for specific database
18 * tables, such {@link \MediaWiki\Page\PageSelectQueryBuilder}, whilst still
19 * providing the same fluent interface for adding arbitrary additional
20 * conditions and such.
21 *
22 * @since 1.35
23 * @stable to extend
24 * @ingroup Database
25 */
26class SelectQueryBuilder extends JoinGroupBase {
27
28    /** sort the results in ascending order */
29    public const SORT_ASC = 'ASC';
30
31    /** sort the results in descending order */
32    public const SORT_DESC = 'DESC';
33
34    /**
35     * @var array The fields to be passed to IReadableDatabase::select()
36     */
37    private $fields = [];
38
39    /**
40     * @var array Like $fields, but always an assoc array, for checking which field/alias names are already used
41     */
42    private $aliasesUsed = [];
43
44    /**
45     * @var array The conditions to be passed to IReadableDatabase::select()
46     */
47    private $conds = [];
48
49    /**
50     * @var string The caller (function name) to be passed to IReadableDatabase::select()
51     */
52    private $caller = __CLASS__;
53
54    /**
55     * @var array The options to be passed to IReadableDatabase::select()
56     */
57    protected $options = [];
58
59    /**
60     * @var int An integer used to assign automatic aliases to tables and groups
61     */
62    private $nextAutoAlias = 1;
63
64    /**
65     * @var bool True if $this->caller has been set
66     */
67    private $isCallerOverridden = false;
68
69    protected IReadableDatabase $db;
70
71    /**
72     * Only for use in subclasses. To create a SelectQueryBuilder instance,
73     * use `$db->newSelectQueryBuilder()` instead.
74     */
75    public function __construct( IReadableDatabase $db ) {
76        $this->db = $db;
77    }
78
79    /**
80     * Change the IReadableDatabase object the query builder is bound to. The specified
81     * IReadableDatabase will subsequently be used to execute the query.
82     *
83     * @param IReadableDatabase $db
84     * @return $this
85     */
86    public function connection( IReadableDatabase $db ) {
87        if ( $this->db->getType() !== $db->getType() ) {
88            throw new \InvalidArgumentException( __METHOD__ .
89                ' cannot switch to a database of a different type.' );
90        }
91        $this->db = $db;
92        return $this;
93    }
94
95    /**
96     * Set the query parameters to the given values, appending to the values
97     * which were already set. This can be used to interface with legacy code.
98     * If a key is omitted, the previous value will be retained.
99     *
100     * The parameters must be formatted as required by IReadableDatabase::select. For
101     * example, JoinGroup cannot be used.
102     *
103     * @param array $info Associative array of query info, with keys:
104     *   - tables: The raw array of tables to be passed to IReadableDatabase::select()
105     *   - fields: The fields
106     *   - conds: The conditions
107     *   - options: The query options
108     *   - join_conds: The join conditions
109     *   - joins: Alias for join_conds. If both joins and join_conds are
110     *     specified, the values will be merged.
111     *   - caller: The caller signature
112     *
113     * @return $this
114     */
115    public function queryInfo( $info ) {
116        if ( isset( $info['tables'] ) ) {
117            $this->rawTables( $info['tables'] );
118        }
119        if ( isset( $info['fields'] ) ) {
120            $this->fields( $info['fields'] );
121        }
122        if ( isset( $info['conds'] ) ) {
123            $this->where( $info['conds'] );
124        }
125        if ( isset( $info['options'] ) ) {
126            $this->options( (array)$info['options'] );
127        }
128        if ( isset( $info['join_conds'] ) ) {
129            $this->joinConds( (array)$info['join_conds'] );
130        }
131        if ( isset( $info['joins'] ) ) {
132            $this->joinConds( (array)$info['joins'] );
133        }
134        if ( isset( $info['caller'] ) ) {
135            $this->caller( $info['caller'] );
136        }
137        return $this;
138    }
139
140    /**
141     * Given a table or table array as might be passed to IReadableDatabase::select(),
142     * append it to the existing tables, interpreting nested arrays as join
143     * groups.
144     *
145     * This can be used to interface with existing code that expresses join
146     * groups as nested arrays. In new code, join groups should generally
147     * be created with newJoinGroup(), which provides a fluent interface.
148     *
149     * @param string|array $tables Table references; see {@link IReadableDatabase::select}
150     *  for details
151     * @return $this
152     */
153    public function rawTables( $tables ) {
154        if ( is_array( $tables ) ) {
155            $this->tables = array_merge( $this->tables, $tables );
156        } elseif ( is_string( $tables ) ) {
157            $this->tables[] = $tables;
158        } else {
159            throw new \InvalidArgumentException( __METHOD__ .
160                ': $tables must be a string or array' );
161        }
162        return $this;
163    }
164
165    /**
166     * Merge another query builder with this one. Append the other builder's
167     * tables, joins, fields, conditions and options to this one.
168     *
169     * @since 1.41
170     * @param SelectQueryBuilder $builder
171     * @return $this
172     */
173    public function merge( SelectQueryBuilder $builder ) {
174        $this->rawTables( $builder->tables );
175        $this->fields( $builder->fields );
176        $this->where( $builder->conds );
177        $this->options( $builder->options );
178        $this->joinConds( $builder->joinConds );
179        if ( $builder->isCallerOverridden ) {
180            $this->caller( $builder->caller );
181        }
182        return $this;
183    }
184
185    /**
186     * Get an empty SelectQueryBuilder which can be used to build a subquery
187     * of this query.
188     * @return SelectQueryBuilder
189     */
190    public function newSubquery() {
191        return new self( $this->db );
192    }
193
194    /**
195     * Add a single table to the SELECT query. Alias for table().
196     *
197     * @param string|JoinGroup|Subquery|SelectQueryBuilder $table Table reference; see {@link table}
198     *  for details
199     * @param-taint $table exec_sql
200     * @param string|null $alias The table alias, or null for no alias
201     * @param-taint $alias exec_sql
202     * @return $this
203     */
204    public function from( $table, $alias = null ) {
205        return $this->table( $table, $alias );
206    }
207
208    /**
209     * Add multiple tables. It's recommended to use join() and leftJoin() instead in new code.
210     *
211     * @param string[] $tables Table references (string keys are aliases). See {@link table}
212     *  for details.
213     * @param-taint $tables exec_sql
214     * @return $this
215     */
216    public function tables( $tables ) {
217        foreach ( $tables as $alias => $table ) {
218            if ( is_string( $alias ) ) {
219                $this->table( $table, $alias );
220            } else {
221                $this->table( $table );
222            }
223        }
224        return $this;
225    }
226
227    /**
228     * Add a field or an array of fields to the query. Each field is an SQL
229     * fragment. If the array key is non-numeric, the key is taken to be an
230     * alias for the field.
231     *
232     * @see IReadableDatabase::select()
233     *
234     * @param string|string[] $fields
235     * @param-taint $fields exec_sql
236     * @return $this
237     */
238    public function fields( $fields ) {
239        if ( !is_array( $fields ) ) {
240            return $this->field( $fields );
241        }
242
243        foreach ( $fields as $alias => $field ) {
244            $this->field( $field, is_numeric( $alias ) ? null : $alias );
245        }
246
247        return $this;
248    }
249
250    /**
251     * Add a field or an array of fields to the query. Alias for fields().
252     *
253     * @param string|string[] $fields
254     * @param-taint $fields exec_sql
255     * @return $this
256     */
257    public function select( $fields ) {
258        return $this->fields( $fields );
259    }
260
261    /**
262     * Add a single field to the query, optionally with an alias. The field is
263     * an SQL fragment. It is unsafe to pass user input to this function.
264     *
265     * @param string $field
266     * @param-taint $field exec_sql
267     * @param string|null $alias
268     * @param-taint $alias exec_sql
269     * @return $this
270     */
271    public function field( $field, $alias = null ) {
272        $usedAlias = $alias ?? $field;
273        if ( isset( $this->aliasesUsed[$usedAlias] ) ) {
274            if ( $this->aliasesUsed[$usedAlias] !== $field ) {
275                throw new \LogicException( __METHOD__ .
276                    ": field alias \"$usedAlias\" is already used for a different field" );
277            }
278            // Identical field/alias combination was already added, don't add it again
279            return $this;
280        }
281        $this->aliasesUsed[$usedAlias] = $field;
282
283        if ( $alias === null ) {
284            $this->fields[] = $field;
285        } else {
286            $this->fields[$alias] = $field;
287        }
288        return $this;
289    }
290
291    /**
292     * Remove all fields from the query.
293     *
294     * @return $this
295     */
296    public function clearFields() {
297        $this->fields = [];
298        $this->aliasesUsed = [];
299        return $this;
300    }
301
302    /**
303     * Add conditions to the query. The supplied conditions will be appended
304     * to the existing conditions, separated by AND.
305     *
306     * @phpcs:ignore Generic.Files.LineLength
307     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
308     * @param-taint $conds exec_sql_numkey
309     *
310     * May be either a string containing a single condition, or an array of
311     * conditions. If an array is given, the conditions constructed from each
312     * element are combined with AND.
313     *
314     * Array elements may take one of two forms:
315     *
316     *   - Elements with a numeric key are interpreted as raw SQL fragments.
317     *   - Elements with a string key are interpreted as equality conditions,
318     *     where the key is the field name.
319     *     - If the value of such an array element is a scalar (such as a
320     *       string), it will be treated as data and thus quoted appropriately.
321     *       If it is null, an IS NULL clause will be added.
322     *     - If the value is an array, an IN (...) clause will be constructed
323     *       from its non-null elements, and an IS NULL clause will be added
324     *       if null is present, such that the field may match any of the
325     *       elements in the array. The non-null elements will be quoted.
326     *
327     * Note that expressions are often DBMS-dependent in their syntax.
328     * DBMS-independent wrappers are provided for constructing several types of
329     * expression commonly used in condition queries. See:
330     *    - IReadableDatabase::buildLike()
331     *    - IReadableDatabase::conditional()
332     *
333     * Untrusted user input is safe in the values of string keys, however untrusted
334     * input must not be used in the array key names or in the values of numeric keys.
335     * Escaping of untrusted input used in values of numeric keys should be done via
336     * IReadableDatabase::addQuotes()
337     *
338     * @return $this
339     */
340    public function where( $conds ) {
341        if ( is_array( $conds ) ) {
342            foreach ( $conds as $key => $cond ) {
343                if ( is_int( $key ) ) {
344                    $this->conds[] = $cond;
345                } elseif ( isset( $this->conds[$key] ) ) {
346                    // @phan-suppress-previous-line PhanTypeMismatchDimFetch
347                    // T288882
348                    $this->conds[] = $this->db->makeList(
349                        [ $key => $cond ], IReadableDatabase::LIST_AND );
350                } else {
351                    $this->conds[$key] = $cond;
352                }
353            }
354        } else {
355            $this->conds[] = $conds;
356        }
357        return $this;
358    }
359
360    /**
361     * Add conditions to the query. Alias for where().
362     *
363     * @phpcs:ignore Generic.Files.LineLength
364     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
365     * @param-taint $conds exec_sql_numkey
366     * @return $this
367     */
368    public function andWhere( $conds ) {
369        return $this->where( $conds );
370    }
371
372    /**
373     * Add conditions to the query. Alias for where().
374     *
375     * @phpcs:ignore Generic.Files.LineLength
376     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
377     * @param-taint $conds exec_sql_numkey
378     * @return $this
379     */
380    public function conds( $conds ) {
381        return $this->where( $conds );
382    }
383
384    /**
385     * Manually append to the $join_conds array which will be passed to
386     * IReadableDatabase::select(). This is not recommended for new code. Instead,
387     * join() and leftJoin() should be used.
388     *
389     * @param array $joinConds
390     * @return $this
391     */
392    public function joinConds( array $joinConds ) {
393        $this->joinConds = array_merge( $this->joinConds, $joinConds );
394        return $this;
395    }
396
397    /**
398     * Get a table alias which is unique to this SelectQueryBuilder
399     *
400     * @return string
401     */
402    protected function getAutoAlias() {
403        return 'sqb' . ( $this->nextAutoAlias++ );
404    }
405
406    /**
407     * Create a parenthesized group of joins which can be added to the object
408     * like a table. The group is initially empty.
409     *
410     * @return JoinGroup
411     */
412    public function newJoinGroup() {
413        return new JoinGroup( $this->getAutoAlias() );
414    }
415
416    /**
417     * Set the offset. Skip this many rows at the start of the result set. Offset
418     * with limit() can theoretically be used for paging through a result set,
419     * but this is discouraged for performance reasons.
420     *
421     * If the query builder already has an offset, the old offset will be discarded.
422     *
423     * @param int $offset
424     * @return $this
425     */
426    public function offset( $offset ) {
427        $this->options['OFFSET'] = $offset;
428        return $this;
429    }
430
431    /**
432     * Set the query limit. Return at most this many rows. The rows are sorted
433     * and then the first rows are taken until the limit is reached. Limit
434     * is applied to a result set after offset.
435     *
436     * If the query builder already has a limit, the old limit will be discarded.
437     *
438     * @param int $limit
439     * @return $this
440     */
441    public function limit( $limit ) {
442        $this->options['LIMIT'] = $limit;
443        return $this;
444    }
445
446    /**
447     * Enable the LOCK IN SHARE MODE option. Lock the returned rows so that
448     * they can't be changed until the next COMMIT. Cannot be used with
449     * aggregate functions (COUNT, MAX, etc., but also DISTINCT).
450     *
451     * @return $this
452     */
453    public function lockInShareMode() {
454        $this->options[] = 'LOCK IN SHARE MODE';
455        return $this;
456    }
457
458    /**
459     * Enable the FOR UPDATE option. Lock the returned rows so that
460     * they can't be changed until the next COMMIT. Cannot be used with
461     * aggregate functions (COUNT, MAX, etc., but also DISTINCT).
462     *
463     * @return $this
464     */
465    public function forUpdate() {
466        $this->options[] = 'FOR UPDATE';
467        return $this;
468    }
469
470    /**
471     * Enable the DISTINCT option. Return only unique result rows.
472     *
473     * @return $this
474     */
475    public function distinct() {
476        $this->options[] = 'DISTINCT';
477        return $this;
478    }
479
480    /**
481     * Set MAX_EXECUTION_TIME for queries.
482     *
483     * @param int $time maximum allowed time in milliseconds
484     * @return $this
485     */
486    public function setMaxExecutionTime( int $time ) {
487        $this->options['MAX_EXECUTION_TIME'] = $time;
488        return $this;
489    }
490
491    /**
492     * Add a GROUP BY clause. May be either an SQL fragment string naming a
493     * field or expression to group by, or an array of such SQL fragments.
494     *
495     * If there is an existing GROUP BY clause, the new one will be appended.
496     *
497     * @param string|string[] $group
498     * @param-taint $group exec_sql
499     * @return $this
500     */
501    public function groupBy( $group ) {
502        $this->mergeOption( 'GROUP BY', $group );
503        return $this;
504    }
505
506    /**
507     * Add a HAVING clause. May be either a string containing a HAVING clause
508     * or an array of conditions building the HAVING clause. If an array is
509     * given, the conditions constructed from each element are combined with
510     * AND.
511     *
512     * If there is an existing HAVING clause, the new one will be appended.
513     *
514     * @param string|string[] $having
515     * @param-taint $having exec_sql_numkey
516     * @return $this
517     */
518    public function having( $having ) {
519        $this->mergeOption( 'HAVING', $having );
520        return $this;
521    }
522
523    /**
524     * Set the ORDER BY clause. If it has already been set, append the
525     * additional fields to it.
526     *
527     * @param string[]|string $fields The field or list of fields to order by.
528     * @param-taint $fields exec_sql
529     * @param string|null $direction Sorting direction applied to all fields,
530     *   self::SORT_ASC or self::SORT_DESC. If different fields need to be sorted in opposite
531     *   directions, then this parameter must be omitted, and $fields must contain 'ASC' or 'DESC'
532     *   after each field name.
533     * @param-taint $direction exec_sql
534     * @return $this
535     */
536    public function orderBy( $fields, $direction = null ) {
537        if ( $direction === null ) {
538            $this->mergeOption( 'ORDER BY', $fields );
539        } elseif ( is_array( $fields ) ) {
540            $fieldsWithDirection = [];
541            foreach ( $fields as $field ) {
542                $fieldsWithDirection[] = "$field $direction";
543            }
544            $this->mergeOption( 'ORDER BY', $fieldsWithDirection );
545        } else {
546            $this->mergeOption( 'ORDER BY', "$fields $direction" );
547        }
548        return $this;
549    }
550
551    /**
552     * Add a value to an option which may be not set or a string or array.
553     *
554     * @param string $name
555     * @param string|string[] $newArrayOrValue
556     */
557    private function mergeOption( $name, $newArrayOrValue ) {
558        $value = isset( $this->options[$name] )
559            ? (array)$this->options[$name] : [];
560        if ( is_array( $newArrayOrValue ) ) {
561            $value = array_merge( $value, $newArrayOrValue );
562        } else {
563            $value[] = $newArrayOrValue;
564        }
565        $this->options[$name] = $value;
566    }
567
568    /**
569     * Set a USE INDEX option.
570     *
571     * If a string is given, the index hint is applied to the most recently
572     * appended table or alias. If an array is given, it is assumed to be an
573     * associative array with the alias names in the keys and the indexes in
574     * the values, as in the USE INDEX option to IReadableDatabase::select(). The
575     * array will be merged with the existing value.
576     *
577     * @param string|string[] $index
578     * @param-taint $index exec_sql
579     * @return $this
580     */
581    public function useIndex( $index ) {
582        $this->setIndexHint( 'USE INDEX', $index );
583        return $this;
584    }
585
586    /**
587     * Set the IGNORE INDEX option.
588     *
589     * If a string is given, the index hint is applied to the most recently
590     * appended table or alias. If an array is given, it is assumed to be an
591     * associative array with the alias names in the keys and the indexes in
592     * the values, as in the IGNORE INDEX option to IReadableDatabase::select(). The
593     * array will be merged with the existing value.
594     *
595     * @param string|string[] $index
596     * @param-taint $index exec_sql
597     * @return $this
598     */
599    public function ignoreIndex( $index ) {
600        $this->setIndexHint( 'IGNORE INDEX', $index );
601        return $this;
602    }
603
604    /**
605     * Private helper for methods that set index hints.
606     *
607     * @param string $type
608     * @param string|string[] $value
609     */
610    private function setIndexHint( $type, $value ) {
611        if ( !isset( $this->options[$type] ) ) {
612            $this->options[$type] = [];
613        } elseif ( !is_array( $this->options[$type] ) ) {
614            throw new \UnexpectedValueException(
615                __METHOD__ . ": The $type option cannot be appended to " .
616                'because it is not an array. This may have been caused by a prior ' .
617                'call to option() or options().' );
618        }
619        if ( is_array( $value ) ) {
620            $this->options[$type] = array_merge( $this->options[$type], $value );
621        } elseif ( $this->lastAlias === null ) {
622            throw new \UnexpectedValueException(
623                __METHOD__ . ': Cannot append index value since there is no' .
624                'prior table' );
625        } else {
626            $this->options[$type][$this->lastAlias] = $value;
627        }
628    }
629
630    /**
631     * Make the query be an EXPLAIN SELECT query instead of a SELECT query.
632     *
633     * @return $this
634     */
635    public function explain() {
636        $this->options['EXPLAIN'] = true;
637        return $this;
638    }
639
640    /**
641     * Enable the STRAIGHT_JOIN query option.
642     *
643     * @return $this
644     */
645    public function straightJoinOption() {
646        $this->options[] = 'STRAIGHT_JOIN';
647        return $this;
648    }
649
650    /**
651     * Enable the SQL_BIG_RESULT option.
652     *
653     * @return $this
654     */
655    public function bigResult() {
656        $this->options[] = 'SQL_BIG_RESULT';
657        return $this;
658    }
659
660    /**
661     * Enable the SQL_BUFFER_RESULT option.
662     *
663     * @return $this
664     */
665    public function bufferResult() {
666        $this->options[] = 'SQL_BUFFER_RESULT';
667        return $this;
668    }
669
670    /**
671     * Enable the SQL_SMALL_RESULT option.
672     *
673     * @return $this
674     */
675    public function smallResult() {
676        $this->options[] = 'SQL_SMALL_RESULT';
677        return $this;
678    }
679
680    /**
681     * Enable the SQL_CALC_FOUND_ROWS option.
682     *
683     * @return $this
684     */
685    public function calcFoundRows() {
686        $this->options[] = 'SQL_CALC_FOUND_ROWS';
687        return $this;
688    }
689
690    /**
691     * Manually set an option in the $options array to be passed to
692     * IReadableDatabase::select()
693     *
694     * @param string $name The option name
695     * @param mixed $value The option value, or null for a boolean option
696     * @return $this
697     */
698    public function option( $name, $value = null ) {
699        if ( $value === null ) {
700            $this->options[] = $name;
701        } else {
702            $this->options[$name] = $value;
703        }
704        return $this;
705    }
706
707    /**
708     * Manually set multiple options in the $options array to be passed to
709     * IReadableDatabase::select().
710     *
711     * @param array $options
712     * @return $this
713     */
714    public function options( array $options ) {
715        $this->options = array_merge( $this->options, $options );
716        return $this;
717    }
718
719    /**
720     * @param int $recency Bitfield of IDBAccessObject::READ_* constants
721     * @return $this
722     */
723    public function recency( $recency ) {
724        if ( ( $recency & IDBAccessObject::READ_EXCLUSIVE ) == IDBAccessObject::READ_EXCLUSIVE ) {
725            $this->forUpdate();
726        } elseif ( ( $recency & IDBAccessObject::READ_LOCKING ) == IDBAccessObject::READ_LOCKING ) {
727            $this->lockInShareMode();
728        }
729        return $this;
730    }
731
732    /**
733     * Set the method name to be included in an SQL comment.
734     *
735     * @param string $fname
736     * @param-taint $fname exec_sql
737     * @return $this
738     */
739    public function caller( $fname ) {
740        $this->caller = $fname;
741        $this->isCallerOverridden = true;
742        return $this;
743    }
744
745    /**
746     * get the method name of the caller, for use in sub classes
747     *
748     * @since 1.43
749     */
750    final protected function getCaller(): string {
751        return $this->caller;
752    }
753
754    /**
755     * Run the constructed SELECT query and return all results.
756     *
757     * @return IResultWrapper
758     * @return-taint tainted
759     */
760    public function fetchResultSet(): IResultWrapper {
761        return $this->db->select( $this->tables, $this->fields, $this->conds, $this->caller,
762            $this->options, $this->joinConds );
763    }
764
765    /**
766     * Run the constructed SELECT query, and return a single field extracted
767     * from the first result row. This may only be called when only one field
768     * has been added to the builder.
769     *
770     * @return mixed|false The value from the field, or false if nothing was found
771     * @return-taint tainted
772     */
773    public function fetchField() {
774        if ( count( $this->fields ) !== 1 ) {
775            throw new \UnexpectedValueException(
776                __METHOD__ . ' expects the query to have only one field' );
777        }
778        $field = reset( $this->fields );
779        return $this->db->selectField( $this->tables, $field, $this->conds, $this->caller,
780            $this->options, $this->joinConds );
781    }
782
783    /**
784     * Run the constructed SELECT query, and extract a single field from each
785     * result row, returning an array containing all the values. This may only
786     * be called when only one field has been added to the builder.
787     *
788     * @return array
789     * @return-taint tainted
790     */
791    public function fetchFieldValues(): array {
792        if ( count( $this->fields ) !== 1 ) {
793            throw new \UnexpectedValueException(
794                __METHOD__ . ' expects the query to have only one field' );
795        }
796        $field = reset( $this->fields );
797        return $this->db->selectFieldValues( $this->tables, $field, $this->conds, $this->caller,
798            $this->options, $this->joinConds );
799    }
800
801    /**
802     * Run the constructed SELECT query, and return the first result row. If
803     * there were no results, return false.
804     *
805     * @return \stdClass|false
806     * @return-taint tainted
807     */
808    public function fetchRow() {
809        return $this->db->selectRow( $this->tables, $this->fields, $this->conds, $this->caller,
810            $this->options, $this->joinConds );
811    }
812
813    /**
814     * Run the SELECT query, and return the number of results. This typically
815     * uses a subquery to discard the actual results on the server side, and
816     * is useful when counting rows with a limit.
817     *
818     * To count rows without a limit, it's more efficient to use a normal
819     * COUNT() expression, for example:
820     *
821     *   $queryBuilder->select( 'COUNT(*)' )->from( 'page' )->fetchField()
822     */
823    public function fetchRowCount(): int {
824        return $this->db->selectRowCount( $this->tables, $this->getRowCountVar(), $this->conds,
825            $this->caller, $this->options, $this->joinConds );
826    }
827
828    /**
829     * Estimate the number of rows in dataset
830     *
831     * MySQL allows you to estimate the number of rows that would be returned
832     * by a SELECT query, using EXPLAIN SELECT. The estimate is provided using
833     * index cardinality statistics, and is notoriously inaccurate, especially
834     * when large numbers of rows have recently been added or deleted.
835     */
836    public function estimateRowCount(): int {
837        return $this->db->estimateRowCount( $this->tables, $this->getRowCountVar(), $this->conds,
838            $this->caller, $this->options, $this->joinConds );
839    }
840
841    /**
842     * Private helper which extracts a field suitable for row counting from the
843     * fields array
844     *
845     * @return string
846     */
847    private function getRowCountVar() {
848        if ( count( $this->fields ) === 0 ) {
849            return '*';
850        } elseif ( count( $this->fields ) === 1 ) {
851            return reset( $this->fields );
852        } else {
853            throw new \UnexpectedValueException(
854                __METHOD__ . ' expects the query to have at most one field' );
855        }
856    }
857
858    /**
859     * Build a GROUP_CONCAT or equivalent statement for a query.
860     *
861     * This is useful for combining a field for several rows into a single string.
862     * NULL values will not appear in the output, duplicated values will appear,
863     * and the resulting delimiter-separated values have no defined sort order.
864     * Code using the results may need to use the PHP unique() or sort() methods.
865     *
866     * @param string $delim
867     * @return string
868     */
869    public function buildGroupConcatField( $delim ) {
870        if ( count( $this->fields ) !== 1 ) {
871            throw new \UnexpectedValueException(
872                __METHOD__ . ' expects the query to have only one field' );
873        }
874        $field = reset( $this->fields );
875        return $this->db->buildGroupConcatField( $delim, $this->tables, $field,
876            $this->conds, $this->joinConds );
877    }
878
879    /**
880     * Get the SQL query string which would be used by fetchResultSet().
881     *
882     * @return string
883     */
884    public function getSQL() {
885        // Assume that whoever is calling this method is doing it to build a subquery
886        $caller = $this->isCallerOverridden ? $this->caller : ISQLPlatform::CALLER_SUBQUERY;
887        return $this->db->selectSQLText( $this->tables, $this->fields, $this->conds, $caller,
888            $this->options, $this->joinConds );
889    }
890
891    /**
892     * Get an associative array describing the query in terms of its raw parameters to
893     * IReadableDatabase::select(). This can be used to interface with legacy code.
894     *
895     * @param string $joinsName The name of the join_conds key
896     * @return array The query info array, with keys:
897     *   - tables: The table array
898     *   - fields: The fields
899     *   - conds: The conditions
900     *   - options: The query options
901     *   - join_conds: The join conditions. This can also be given a different
902     *     name by passing a $joinsName parameter, since some legacy code uses
903     *     the name "joins".
904     *   - caller: The caller signature
905     */
906    public function getQueryInfo( $joinsName = 'join_conds' ) {
907        $info = [
908            'tables' => $this->tables,
909            'fields' => $this->fields,
910            'conds' => $this->conds,
911            'options' => $this->options,
912        ];
913        if ( $this->caller !== __CLASS__ ) {
914            $info['caller'] = $this->caller;
915        }
916        $info[ $joinsName ] = $this->joinConds;
917        return $info;
918    }
919
920    /**
921     * Execute the query, but throw away the results. This is intended for
922     * locking select queries. By calling this method, the caller is indicating
923     * that the query is only done to acquire locks on the selected rows. The
924     * field list is optional.
925     *
926     * Either forUpdate() or lockInShareMode() must be called before calling
927     * this method.
928     *
929     * @see self::forUpdate()
930     * @see self::lockInShareMode()
931     *
932     * @since 1.40
933     */
934    public function acquireRowLocks(): void {
935        if ( !array_intersect( $this->options, [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] ) ) {
936            throw new \UnexpectedValueException( __METHOD__ . ' can only be called ' .
937                'after forUpdate() or lockInShareMode()' );
938        }
939        $fields = $this->fields ?: '1';
940        $this->db->select( $this->tables, $fields, $this->conds, $this->caller,
941            $this->options, $this->joinConds );
942    }
943}