Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.74% covered (warning)
71.74%
33 / 46
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteQueryBuilder
71.74% covered (warning)
71.74%
33 / 46
58.33% covered (warning)
58.33%
7 / 12
34.94
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
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 table
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteFrom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
 caller
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 getQueryInfo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use InvalidArgumentException;
6use UnexpectedValueException;
7
8// Very long type annotations :(
9// phpcs:disable Generic.Files.LineLength
10
11/**
12 * A query builder for DELETE queries with a fluent interface.
13 *
14 * Any particular query builder object should only be used for a single database query,
15 * and not be reused afterwards. However, to run multiple similar queries,
16 * you can create a “template” query builder to set up most of the query,
17 * and then clone the object (and potentially modify the clone) for each individual query.
18 *
19 * @stable to extend
20 * @since 1.41
21 * @ingroup Database
22 */
23class DeleteQueryBuilder {
24    /**
25     * The table name to be passed to IDatabase::delete()
26     */
27    private string $table = '';
28
29    /**
30     * The conditions to be passed to IDatabase::delete()
31     */
32    private array $conds = [];
33
34    /**
35     * The caller (function name) to be passed to IDatabase::delete()
36     */
37    private string $caller = __CLASS__;
38
39    protected IDatabase $db;
40
41    /**
42     * Only for use in subclasses and Database::newDeleteQueryBuilder.
43     * To create a DeleteQueryBuilder instance, use `$db->newDeleteQueryBuilder()` instead.
44     *
45     * @param IDatabase $db
46     */
47    public function __construct( IDatabase $db ) {
48        $this->db = $db;
49    }
50
51    /**
52     * Change the IDatabase object the query builder is bound to. The specified
53     * IDatabase will subsequently be used to execute the query.
54     *
55     * @param IDatabase $db
56     * @return $this
57     */
58    public function connection( IDatabase $db ): DeleteQueryBuilder {
59        if ( $this->db->getType() !== $db->getType() ) {
60            throw new InvalidArgumentException(
61                __METHOD__ . ' cannot switch to a database of a different type.'
62            );
63        }
64        $this->db = $db;
65        return $this;
66    }
67
68    /**
69     * Set the query parameters to the given values, appending to the values
70     * which were already set. This can be used to interface with legacy code.
71     * If a key is omitted, the previous value will be retained.
72     *
73     * The parameters must be formatted as required by Database::delete.
74     *
75     * @param array $info Associative array of query info, with keys:
76     *   - table: The table name to be passed to IDatabase::delete()
77     *   - conds: The conditions
78     *   - caller: The caller signature
79     *
80     * @return $this
81     */
82    public function queryInfo( array $info ): DeleteQueryBuilder {
83        if ( isset( $info['table'] ) ) {
84            $this->table( $info['table'] );
85        }
86        if ( isset( $info['conds'] ) ) {
87            $this->where( $info['conds'] );
88        }
89        if ( isset( $info['caller'] ) ) {
90            $this->caller( $info['caller'] );
91        }
92        return $this;
93    }
94
95    /**
96     * Manually set the table name to be passed to IDatabase::delete()
97     *
98     * @param string $table Unqualified name of table
99     * @param-taint $table exec_sql
100     * @return $this
101     */
102    public function table( string $table ): DeleteQueryBuilder {
103        $this->table = $table;
104        return $this;
105    }
106
107    /**
108     * Set table for the query. Alias for table().
109     *
110     * @param string $table Unqualified name of table
111     * @param-taint $table exec_sql
112     * @return $this
113     */
114    public function deleteFrom( string $table ): DeleteQueryBuilder {
115        return $this->table( $table );
116    }
117
118    /**
119     * Set table for the query. Alias for table().
120     *
121     * @param string $table Unqualified name of table
122     * @param-taint $table exec_sql
123     * @return $this
124     */
125    public function delete( string $table ): DeleteQueryBuilder {
126        return $this->table( $table );
127    }
128
129    /**
130     * Add conditions to the query. The supplied conditions will be appended
131     * to the existing conditions, separated by AND.
132     *
133     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
134     * @param-taint $conds exec_sql_numkey
135     *
136     * May be either a string containing a single condition, or an array of
137     * conditions. If an array is given, the conditions constructed from each
138     * element are combined with AND.
139     *
140     * Array elements may take one of two forms:
141     *
142     *   - Elements with a numeric key are interpreted as raw SQL fragments.
143     *   - Elements with a string key are interpreted as equality conditions,
144     *     where the key is the field name.
145     *     - If the value of such an array element is a scalar (such as a
146     *       string), it will be treated as data and thus quoted appropriately.
147     *       If it is null, an IS NULL clause will be added.
148     *     - If the value is an array, an IN (...) clause will be constructed
149     *       from its non-null elements, and an IS NULL clause will be added
150     *       if null is present, such that the field may match any of the
151     *       elements in the array. The non-null elements will be quoted.
152     *
153     * Note that expressions are often DBMS-dependent in their syntax.
154     * DBMS-independent wrappers are provided for constructing several types of
155     * expression commonly used in condition queries. See:
156     *    - IDatabase::buildLike()
157     *    - IDatabase::conditional()
158     *
159     * Untrusted user input is safe in the values of string keys, however untrusted
160     * input must not be used in the array key names or in the values of numeric keys.
161     * Escaping of untrusted input used in values of numeric keys should be done via
162     * IDatabase::addQuotes()
163     *
164     * @return $this
165     */
166    public function where( $conds ): DeleteQueryBuilder {
167        if ( is_array( $conds ) ) {
168            foreach ( $conds as $key => $cond ) {
169                if ( is_int( $key ) ) {
170                    $this->conds[] = $cond;
171                } elseif ( isset( $this->conds[$key] ) ) {
172                    // @phan-suppress-previous-line PhanTypeMismatchDimFetch
173                    // T288882
174                    $this->conds[] = $this->db->makeList(
175                        [ $key => $cond ], IDatabase::LIST_AND );
176                } else {
177                    $this->conds[$key] = $cond;
178                }
179            }
180        } else {
181            $this->conds[] = $conds;
182        }
183        return $this;
184    }
185
186    /**
187     * Add conditions to the query. Alias for where().
188     *
189     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
190     * @param-taint $conds exec_sql_numkey
191     * @return $this
192     */
193    public function andWhere( $conds ): DeleteQueryBuilder {
194        return $this->where( $conds );
195    }
196
197    /**
198     * Add conditions to the query. Alias for where().
199     *
200     * @param string|IExpression|array<string,?scalar|non-empty-array<int,?scalar>|RawSQLValue>|array<int,string|IExpression> $conds
201     * @param-taint $conds exec_sql_numkey
202     * @return $this
203     */
204    public function conds( $conds ): DeleteQueryBuilder {
205        return $this->where( $conds );
206    }
207
208    /**
209     * Set the method name to be included in an SQL comment.
210     *
211     * @param string $fname
212     * @param-taint $fname exec_sql
213     * @return $this
214     */
215    public function caller( string $fname ): DeleteQueryBuilder {
216        $this->caller = $fname;
217        return $this;
218    }
219
220    /**
221     * Run the constructed DELETE query.
222     */
223    public function execute(): void {
224        if ( !$this->conds ) {
225            throw new UnexpectedValueException(
226                __METHOD__ . ' expects at least one condition to be set' );
227        }
228        if ( $this->table === '' ) {
229            throw new UnexpectedValueException(
230                __METHOD__ . ' expects table not to be empty' );
231        }
232        $this->db->delete( $this->table, $this->conds, $this->caller );
233    }
234
235    /**
236     * Get an associative array describing the query in terms of its raw parameters to
237     * IDatabase::delete(). This can be used to interface with legacy code.
238     *
239     * @return array The query info array, with keys:
240     *   - table: The table name
241     *   - conds: The conditions
242     *   - caller: The caller signature
243     */
244    public function getQueryInfo(): array {
245        $info = [
246            'table' => $this->table,
247            'conds' => $this->conds,
248        ];
249        if ( $this->caller !== __CLASS__ ) {
250            $info['caller'] = $this->caller;
251        }
252        return $info;
253    }
254}