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 | /** |
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 | */ |
21 | class 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 | } |