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 implements IWriteQueryBuilder {
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     * @inheritDoc
59     */
60    public function connection( IDatabase $db ) {
61        if ( $this->db->getType() !== $db->getType() ) {
62            throw new InvalidArgumentException(
63                __METHOD__ . ' cannot switch to a database of a different type.'
64            );
65        }
66        $this->db = $db;
67        return $this;
68    }
69
70    /**
71     * @inheritDoc
72     */
73    public function queryInfo( $info ) {
74        if ( isset( $info['table'] ) ) {
75            $this->table( $info['table'] );
76        }
77        if ( isset( $info['set'] ) ) {
78            $this->set( $info['set'] );
79        }
80        if ( isset( $info['conds'] ) ) {
81            $this->where( $info['conds'] );
82        }
83        if ( isset( $info['options'] ) ) {
84            $this->options( (array)$info['options'] );
85        }
86        if ( isset( $info['caller'] ) ) {
87            $this->caller( $info['caller'] );
88        }
89        return $this;
90    }
91
92    /**
93     * Manually set the table name to be passed to IDatabase::update()
94     *
95     * @param string $table The unqualified name of a table
96     * @param-taint $table exec_sql
97     * @return $this
98     */
99    public function table( $table ) {
100        $this->table = $table;
101        return $this;
102    }
103
104    /**
105     * Set table for the query. Alias for table().
106     *
107     * @param string $table The unqualified name of a table
108     * @param-taint $table exec_sql
109     * @return $this
110     */
111    public function update( string $table ) {
112        return $this->table( $table );
113    }
114
115    /**
116     * Manually set an option in the $options array to be passed to
117     * IDatabase::update()
118     *
119     * @param string $name The option name
120     * @param mixed $value The option value, or null for a boolean option
121     * @return $this
122     */
123    public function option( $name, $value = null ) {
124        if ( $value === null ) {
125            $this->options[] = $name;
126        } else {
127            $this->options[$name] = $value;
128        }
129        return $this;
130    }
131
132    /**
133     * Manually set multiple options in the $options array to be passed to
134     * IDatabase::update().
135     *
136     * @param array $options
137     * @return $this
138     */
139    public function options( array $options ) {
140        $this->options = array_merge( $this->options, $options );
141        return $this;
142    }
143
144    /**
145     * Add conditions to the query. The supplied conditions will be appended
146     * to the existing conditions, separated by AND.
147     *
148     * @phpcs:ignore Generic.Files.LineLength
149     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar|Blob>|RawSQLValue|Blob>|array<int,string|IExpression> $conds
150     *
151     * May be either a string containing a single condition, or an array of
152     * conditions. If an array is given, the conditions constructed from each
153     * element are combined with AND.
154     *
155     * Array elements may take one of two forms:
156     *
157     * - Elements with a numeric key are interpreted as raw SQL fragments.
158     * - Elements with a string key are interpreted as equality conditions,
159     *   where the key is the field name.
160     *   - If the value of such an array element is a scalar (such as a
161     *     string), it will be treated as data and thus quoted appropriately.
162     *     If it is null, an IS NULL clause will be added.
163     *   - If the value is an array, an IN (...) clause will be constructed
164     *     from its non-null elements, and an IS NULL clause will be added
165     *     if null is present, such that the field may match any of the
166     *     elements in the array. The non-null elements will be quoted.
167     *
168     * Note that expressions are often DBMS-dependent in their syntax.
169     * DBMS-independent wrappers are provided for constructing several types of
170     * expression commonly used in condition queries. See:
171     * - {@link ISQLPlatform::buildLike()}
172     * - {@link ISQLPlatform::conditional()}
173     *
174     * Untrusted user input is safe in the values of string keys, however untrusted
175     * input must not be used in the array key names or in the values of numeric keys.
176     * Escaping of untrusted input used in values of numeric keys should be done via
177     * {@link IDatabase::addQuotes()}.
178     *
179     * @param-taint $conds exec_sql_numkey
180     * @return $this
181     */
182    public function where( $conds ) {
183        if ( is_array( $conds ) ) {
184            foreach ( $conds as $key => $cond ) {
185                if ( is_int( $key ) ) {
186                    $this->conds[] = $cond;
187                } elseif ( isset( $this->conds[$key] ) ) {
188                    // @phan-suppress-previous-line PhanTypeMismatchDimFetch
189                    // T288882
190                    $this->conds[] = $this->db->makeList(
191                        [ $key => $cond ], IDatabase::LIST_AND );
192                } else {
193                    $this->conds[$key] = $cond;
194                }
195            }
196        } else {
197            $this->conds[] = $conds;
198        }
199        return $this;
200    }
201
202    /**
203     * Add conditions to the query. Alias for where().
204     *
205     * @phpcs:ignore Generic.Files.LineLength
206     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar|Blob>|RawSQLValue|Blob>|array<int,string|IExpression> $conds
207     * @param-taint $conds exec_sql_numkey
208     * @return $this
209     */
210    public function andWhere( $conds ) {
211        return $this->where( $conds );
212    }
213
214    /**
215     * Add conditions to the query. Alias for where().
216     *
217     * @phpcs:ignore Generic.Files.LineLength
218     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar|Blob>|RawSQLValue|Blob>|array<int,string|IExpression> $conds
219     * @param-taint $conds exec_sql_numkey
220     * @return $this
221     */
222    public function conds( $conds ) {
223        return $this->where( $conds );
224    }
225
226    /**
227     * Add SET part to the query. It takes an array containing arrays of column names map to
228     * the set values.
229     *
230     * @param string|array<string,?scalar|RawSQLValue|Blob>|array<int,string> $set
231     * @param-taint $set exec_sql_numkey
232     *
233     * Combination map/list where each string-keyed entry maps a column
234     * to a literal assigned value and each integer-keyed value is a SQL expression in the
235     * format of a column assignment within UPDATE...SET. The (column => value) entries are
236     * convenient due to automatic value quoting and conversion of null to NULL. The SQL
237     * assignment format is useful for updates like "column = column + X". All assignments
238     * have no defined execution order, so they should not depend on each other. Do not
239     * modify AUTOINCREMENT or UUID columns in assignments.
240     *
241     * Untrusted user input is safe in the values of string keys, however untrusted
242     * input must not be used in the array key names or in the values of numeric keys.
243     * Escaping of untrusted input used in values of numeric keys should be done via
244     * IDatabase::addQuotes()
245     *
246     * @return $this
247     */
248    public function set( $set ) {
249        if ( is_array( $set ) ) {
250            foreach ( $set as $key => $value ) {
251                if ( is_int( $key ) ) {
252                    $this->set[] = $value;
253                } else {
254                    $this->set[$key] = $value;
255                }
256            }
257        } else {
258            $this->set[] = $set;
259        }
260        return $this;
261    }
262
263    /**
264     * Add set values to the query. Alias for set().
265     *
266     * @param string|array<string,?scalar|RawSQLValue|Blob>|array<int,string> $set
267     * @param-taint $set exec_sql_numkey
268     * @return $this
269     */
270    public function andSet( $set ) {
271        return $this->set( $set );
272    }
273
274    /**
275     * Enable the IGNORE option.
276     *
277     * Skip update of rows that would cause unique key conflicts.
278     * IDatabase::affectedRows() can be used to determine how many rows were updated.
279     *
280     * @return $this
281     */
282    public function ignore() {
283        $this->options[] = 'IGNORE';
284        return $this;
285    }
286
287    /**
288     * @inheritDoc
289     */
290    public function caller( $fname ) {
291        $this->caller = $fname;
292        return $this;
293    }
294
295    /**
296     * @inheritDoc
297     */
298    public function execute(): void {
299        if ( !$this->conds ) {
300            throw new UnexpectedValueException(
301                __METHOD__ . ' expects at least one condition to be set' );
302        }
303        if ( !$this->set ) {
304            throw new UnexpectedValueException(
305                __METHOD__ . ' can\t have empty $set value' );
306        }
307        if ( $this->table === '' ) {
308            throw new UnexpectedValueException(
309                __METHOD__ . ' expects table not to be empty' );
310        }
311        $this->db->update( $this->table, $this->set, $this->conds, $this->caller, $this->options );
312    }
313
314    /**
315     * @inheritDoc
316     */
317    public function getQueryInfo() {
318        $info = [
319            'table' => $this->table,
320            'set' => $this->set,
321            'conds' => $this->conds,
322            'options' => $this->options,
323        ];
324        if ( $this->caller !== __CLASS__ ) {
325            $info['caller'] = $this->caller;
326        }
327        return $info;
328    }
329}