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