Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
77.14% |
54 / 70 |
|
62.50% |
10 / 16 |
CRAP | |
0.00% |
0 / 1 |
UpdateQueryBuilder | |
77.14% |
54 / 70 |
|
62.50% |
10 / 16 |
47.80 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
connection | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
queryInfo | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
table | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
update | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
option | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
options | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
where | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 | |||
andWhere | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
conds | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
set | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
andSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
ignore | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
caller | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
40.00% |
4 / 10 |
|
0.00% |
0 / 1 |
7.46 | |||
getQueryInfo | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Wikimedia\Rdbms; |
4 | |
5 | use InvalidArgumentException; |
6 | use 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 | */ |
24 | class 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 | } |