Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.14% covered (warning)
77.14%
54 / 70
62.50% covered (warning)
62.50%
10 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateQueryBuilder
77.14% covered (warning)
77.14%
54 / 70
62.50% covered (warning)
62.50%
10 / 16
47.80
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 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 queryInfo
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 table
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
1 / 1
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
 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
 set
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 andSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ignore
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 caller
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
7.46
 getQueryInfo
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use InvalidArgumentException;
6use UnexpectedValueException;
7
8// Very long type annotations :(
9// phpcs:disable Generic.Files.LineLength
10
11/**
12 * Build UPDATE 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 * @since 1.41
21 * @stable to extend
22 * @ingroup Database
23 */
24class UpdateQueryBuilder {
25    /**
26     * @var string The table name to be passed to IDatabase::update()
27     */
28    private $table = '';
29
30    /**
31     * @var array The set values to be passed to IDatabase::update()
32     */
33    private $set = [];
34
35    /**
36     * @var array The conditions to be passed to IDatabase::update()
37     */
38    private $conds = [];
39
40    /**
41     * @var string The caller (function name) to be passed to IDatabase::update()
42     */
43    private $caller = __CLASS__;
44
45    /**
46     * @var array The options to be passed to IDatabase::update()
47     */
48    protected $options = [];
49
50    /** @var IDatabase */
51    protected $db;
52
53    /**
54     * Only for use in subclasses. To create a UpdateQueryBuilder instance,
55     * use `$db->newUpdateQueryBuilder()` instead.
56     *
57     * @param IDatabase $db
58     */
59    public function __construct( IDatabase $db ) {
60        $this->db = $db;
61    }
62
63    /**
64     * Change the IDatabase object the query builder is bound to. The specified
65     * IDatabase will subsequently be used to execute the query.
66     *
67     * @param IDatabase $db
68     * @return $this
69     */
70    public function connection( IDatabase $db ) {
71        if ( $this->db->getType() !== $db->getType() ) {
72            throw new InvalidArgumentException(
73                __METHOD__ . ' cannot switch to a database of a different type.'
74            );
75        }
76        $this->db = $db;
77        return $this;
78    }
79
80    /**
81     * Set the query parameters to the given values, appending to the values
82     * which were already set. This can be used to interface with legacy code.
83     * If a key is omitted, the previous value will be retained.
84     *
85     * The parameters must be formatted as required by Database::update.
86     *
87     * @param array $info Associative array of query info, with keys:
88     *   - table: The table name to be passed to Database::update()
89     *   - set: The set conditions
90     *   - conds: The conditions
91     *   - options: The query options
92     *   - caller: The caller signature.
93     *
94     * @return $this
95     */
96    public function queryInfo( $info ) {
97        if ( isset( $info['table'] ) ) {
98            $this->table( $info['table'] );
99        }
100        if ( isset( $info['set'] ) ) {
101            $this->set( $info['set'] );
102        }
103        if ( isset( $info['conds'] ) ) {
104            $this->where( $info['conds'] );
105        }
106        if ( isset( $info['options'] ) ) {
107            $this->options( (array)$info['options'] );
108        }
109        if ( isset( $info['caller'] ) ) {
110            $this->caller( $info['caller'] );
111        }
112        return $this;
113    }
114
115    /**
116     * Manually set the table name to be passed to IDatabase::update()
117     *
118     * @param string $table The unqualified name of a table
119     * @param-taint $table exec_sql
120     * @return $this
121     */
122    public function table( $table ) {
123        $this->table = $table;
124        return $this;
125    }
126
127    /**
128     * Set table for the query. Alias for table().
129     *
130     * @param string $table The unqualified name of a table
131     * @param-taint $table exec_sql
132     * @return $this
133     */
134    public function update( string $table ) {
135        return $this->table( $table );
136    }
137
138    /**
139     * Manually set an option in the $options array to be passed to
140     * IDatabase::update()
141     *
142     * @param string $name The option name
143     * @param mixed $value The option value, or null for a boolean option
144     * @return $this
145     */
146    public function option( $name, $value = null ) {
147        if ( $value === null ) {
148            $this->options[] = $name;
149        } else {
150            $this->options[$name] = $value;
151        }
152        return $this;
153    }
154
155    /**
156     * Manually set multiple options in the $options array to be passed to
157     * IDatabase::update().
158     *
159     * @param array $options
160     * @return $this
161     */
162    public function options( array $options ) {
163        $this->options = array_merge( $this->options, $options );
164        return $this;
165    }
166
167    /**
168     * Add conditions to the query. The supplied conditions will be appended
169     * to the existing conditions, separated by AND.
170     *
171     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
172     * @param-taint $conds exec_sql_numkey
173     *
174     * May be either a string containing a single condition, or an array of
175     * conditions. If an array is given, the conditions constructed from each
176     * element are combined with AND.
177     *
178     * Array elements may take one of two forms:
179     *
180     *   - Elements with a numeric key are interpreted as raw SQL fragments.
181     *   - Elements with a string key are interpreted as equality conditions,
182     *     where the key is the field name.
183     *     - If the value of such an array element is a scalar (such as a
184     *       string), it will be treated as data and thus quoted appropriately.
185     *       If it is null, an IS NULL clause will be added.
186     *     - If the value is an array, an IN (...) clause will be constructed
187     *       from its non-null elements, and an IS NULL clause will be added
188     *       if null is present, such that the field may match any of the
189     *       elements in the array. The non-null elements will be quoted.
190     *
191     * Note that expressions are often DBMS-dependent in their syntax.
192     * DBMS-independent wrappers are provided for constructing several types of
193     * expression commonly used in condition queries. See:
194     *    - IDatabase::buildLike()
195     *    - IDatabase::conditional()
196     *
197     * Untrusted user input is safe in the values of string keys, however untrusted
198     * input must not be used in the array key names or in the values of numeric keys.
199     * Escaping of untrusted input used in values of numeric keys should be done via
200     * IDatabase::addQuotes()
201     *
202     * @return $this
203     */
204    public function where( $conds ) {
205        if ( is_array( $conds ) ) {
206            foreach ( $conds as $key => $cond ) {
207                if ( is_int( $key ) ) {
208                    $this->conds[] = $cond;
209                } elseif ( isset( $this->conds[$key] ) ) {
210                    // @phan-suppress-previous-line PhanTypeMismatchDimFetch
211                    // T288882
212                    $this->conds[] = $this->db->makeList(
213                        [ $key => $cond ], IDatabase::LIST_AND );
214                } else {
215                    $this->conds[$key] = $cond;
216                }
217            }
218        } else {
219            $this->conds[] = $conds;
220        }
221        return $this;
222    }
223
224    /**
225     * Add conditions to the query. Alias for where().
226     *
227     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
228     * @param-taint $conds exec_sql_numkey
229     * @return $this
230     */
231    public function andWhere( $conds ) {
232        return $this->where( $conds );
233    }
234
235    /**
236     * Add conditions to the query. Alias for where().
237     *
238     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
239     * @param-taint $conds exec_sql_numkey
240     * @return $this
241     */
242    public function conds( $conds ) {
243        return $this->where( $conds );
244    }
245
246    /**
247     * Add SET part to the query. It takes an array containing arrays of column names map to
248     * the set values.
249     *
250     * @param string|array<string,?scalar|RawSQLValue>|array<int,string> $set
251     * @param-taint $set exec_sql_numkey
252     *
253     * Combination map/list where each string-keyed entry maps a column
254     * to a literal assigned value and each integer-keyed value is a SQL expression in the
255     * format of a column assignment within UPDATE...SET. The (column => value) entries are
256     * convenient due to automatic value quoting and conversion of null to NULL. The SQL
257     * assignment format is useful for updates like "column = column + X". All assignments
258     * have no defined execution order, so they should not depend on each other. Do not
259     * modify AUTOINCREMENT or UUID columns in assignments.
260     *
261     * Untrusted user input is safe in the values of string keys, however untrusted
262     * input must not be used in the array key names or in the values of numeric keys.
263     * Escaping of untrusted input used in values of numeric keys should be done via
264     * IDatabase::addQuotes()
265     *
266     * @return $this
267     */
268    public function set( $set ) {
269        if ( is_array( $set ) ) {
270            foreach ( $set as $key => $value ) {
271                if ( is_int( $key ) ) {
272                    $this->set[] = $value;
273                } else {
274                    $this->set[$key] = $value;
275                }
276            }
277        } else {
278            $this->set[] = $set;
279        }
280        return $this;
281    }
282
283    /**
284     * Add set values to the query. Alias for set().
285     *
286     * @param string|array<string,?scalar|RawSQLValue>|array<int,string> $set
287     * @param-taint $set exec_sql_numkey
288     * @return $this
289     */
290    public function andSet( $set ) {
291        return $this->set( $set );
292    }
293
294    /**
295     * Enable the IGNORE option.
296     *
297     * Skip update of rows that would cause unique key conflicts.
298     * IDatabase::affectedRows() can be used to determine how many rows were updated.
299     *
300     * @return $this
301     */
302    public function ignore() {
303        $this->options[] = 'IGNORE';
304        return $this;
305    }
306
307    /**
308     * Set the method name to be included in an SQL comment.
309     *
310     * @param string $fname
311     * @param-taint $fname exec_sql
312     * @return $this
313     */
314    public function caller( $fname ) {
315        $this->caller = $fname;
316        return $this;
317    }
318
319    /**
320     * Run the constructed UPDATE query.
321     */
322    public function execute(): void {
323        if ( !$this->conds ) {
324            throw new UnexpectedValueException(
325                __METHOD__ . ' expects at least one condition to be set' );
326        }
327        if ( !$this->set ) {
328            throw new UnexpectedValueException(
329                __METHOD__ . ' can\t have empty $set value' );
330        }
331        if ( $this->table === '' ) {
332            throw new UnexpectedValueException(
333                __METHOD__ . ' expects table not to be empty' );
334        }
335        $this->db->update( $this->table, $this->set, $this->conds, $this->caller, $this->options );
336    }
337
338    /**
339     * Get an associative array describing the query in terms of its raw parameters to
340     * Database::update(). This can be used to interface with legacy code.
341     *
342     * @return array The query info array, with keys:
343     *   - table: The table name
344     *   - set: The set array
345     *   - conds: The conditions
346     *   - options: The query options
347     *   - caller: The caller signature
348     */
349    public function getQueryInfo() {
350        $info = [
351            'table' => $this->table,
352            'set' => $this->set,
353            'conds' => $this->conds,
354            'options' => $this->options,
355        ];
356        if ( $this->caller !== __CLASS__ ) {
357            $info['caller'] = $this->caller;
358        }
359        return $info;
360    }
361}