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