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    protected IDatabase $db;
48
49    /**
50     * Only for use in subclasses. To create a UpdateQueryBuilder instance,
51     * use `$db->newUpdateQueryBuilder()` instead.
52     */
53    public function __construct( IDatabase $db ) {
54        $this->db = $db;
55    }
56
57    /**
58     * Change the IDatabase object the query builder is bound to. The specified
59     * IDatabase will subsequently be used to execute the query.
60     *
61     * @param IDatabase $db
62     * @return $this
63     */
64    public function connection( IDatabase $db ) {
65        if ( $this->db->getType() !== $db->getType() ) {
66            throw new InvalidArgumentException(
67                __METHOD__ . ' cannot switch to a database of a different type.'
68            );
69        }
70        $this->db = $db;
71        return $this;
72    }
73
74    /**
75     * Set the query parameters to the given values, appending to the values
76     * which were already set. This can be used to interface with legacy code.
77     * If a key is omitted, the previous value will be retained.
78     *
79     * The parameters must be formatted as required by Database::update.
80     *
81     * @param array $info Associative array of query info, with keys:
82     *   - table: The table name to be passed to Database::update()
83     *   - set: The set conditions
84     *   - conds: The conditions
85     *   - options: The query options
86     *   - caller: The caller signature.
87     *
88     * @return $this
89     */
90    public function queryInfo( $info ) {
91        if ( isset( $info['table'] ) ) {
92            $this->table( $info['table'] );
93        }
94        if ( isset( $info['set'] ) ) {
95            $this->set( $info['set'] );
96        }
97        if ( isset( $info['conds'] ) ) {
98            $this->where( $info['conds'] );
99        }
100        if ( isset( $info['options'] ) ) {
101            $this->options( (array)$info['options'] );
102        }
103        if ( isset( $info['caller'] ) ) {
104            $this->caller( $info['caller'] );
105        }
106        return $this;
107    }
108
109    /**
110     * Manually set the table name to be passed to IDatabase::update()
111     *
112     * @param string $table The unqualified name of a table
113     * @param-taint $table exec_sql
114     * @return $this
115     */
116    public function table( $table ) {
117        $this->table = $table;
118        return $this;
119    }
120
121    /**
122     * Set table for the query. Alias for table().
123     *
124     * @param string $table The unqualified name of a table
125     * @param-taint $table exec_sql
126     * @return $this
127     */
128    public function update( string $table ) {
129        return $this->table( $table );
130    }
131
132    /**
133     * Manually set an option in the $options array to be passed to
134     * IDatabase::update()
135     *
136     * @param string $name The option name
137     * @param mixed $value The option value, or null for a boolean option
138     * @return $this
139     */
140    public function option( $name, $value = null ) {
141        if ( $value === null ) {
142            $this->options[] = $name;
143        } else {
144            $this->options[$name] = $value;
145        }
146        return $this;
147    }
148
149    /**
150     * Manually set multiple options in the $options array to be passed to
151     * IDatabase::update().
152     *
153     * @param array $options
154     * @return $this
155     */
156    public function options( array $options ) {
157        $this->options = array_merge( $this->options, $options );
158        return $this;
159    }
160
161    /**
162     * Add conditions to the query. The supplied conditions will be appended
163     * to the existing conditions, separated by AND.
164     *
165     * @phpcs:ignore Generic.Files.LineLength
166     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
167     * @param-taint $conds exec_sql_numkey
168     *
169     * May be either a string containing a single condition, or an array of
170     * conditions. If an array is given, the conditions constructed from each
171     * element are combined with AND.
172     *
173     * Array elements may take one of two forms:
174     *
175     *   - Elements with a numeric key are interpreted as raw SQL fragments.
176     *   - Elements with a string key are interpreted as equality conditions,
177     *     where the key is the field name.
178     *     - If the value of such an array element is a scalar (such as a
179     *       string), it will be treated as data and thus quoted appropriately.
180     *       If it is null, an IS NULL clause will be added.
181     *     - If the value is an array, an IN (...) clause will be constructed
182     *       from its non-null elements, and an IS NULL clause will be added
183     *       if null is present, such that the field may match any of the
184     *       elements in the array. The non-null elements will be quoted.
185     *
186     * Note that expressions are often DBMS-dependent in their syntax.
187     * DBMS-independent wrappers are provided for constructing several types of
188     * expression commonly used in condition queries. See:
189     *    - IDatabase::buildLike()
190     *    - IDatabase::conditional()
191     *
192     * Untrusted user input is safe in the values of string keys, however untrusted
193     * input must not be used in the array key names or in the values of numeric keys.
194     * Escaping of untrusted input used in values of numeric keys should be done via
195     * IDatabase::addQuotes()
196     *
197     * @return $this
198     */
199    public function where( $conds ) {
200        if ( is_array( $conds ) ) {
201            foreach ( $conds as $key => $cond ) {
202                if ( is_int( $key ) ) {
203                    $this->conds[] = $cond;
204                } elseif ( isset( $this->conds[$key] ) ) {
205                    // @phan-suppress-previous-line PhanTypeMismatchDimFetch
206                    // T288882
207                    $this->conds[] = $this->db->makeList(
208                        [ $key => $cond ], IDatabase::LIST_AND );
209                } else {
210                    $this->conds[$key] = $cond;
211                }
212            }
213        } else {
214            $this->conds[] = $conds;
215        }
216        return $this;
217    }
218
219    /**
220     * Add conditions to the query. Alias for where().
221     *
222     * @phpcs:ignore Generic.Files.LineLength
223     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
224     * @param-taint $conds exec_sql_numkey
225     * @return $this
226     */
227    public function andWhere( $conds ) {
228        return $this->where( $conds );
229    }
230
231    /**
232     * Add conditions to the query. Alias for where().
233     *
234     * @phpcs:ignore Generic.Files.LineLength
235     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|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<string,?scalar|RawSQLValue>|array<int,string> $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<string,?scalar|RawSQLValue>|array<int,string> $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}