Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.90% covered (warning)
56.90%
33 / 58
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
UnionQueryBuilder
56.90% covered (warning)
56.90%
33 / 58
38.46% covered (danger)
38.46%
5 / 13
85.38
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
 add
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 all
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 limit
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 offset
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 orderBy
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
9.06
 mergeOption
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 caller
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchResultSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchField
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
5.20
 fetchFieldValues
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 fetchRow
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getSQL
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use Wikimedia\Rdbms\Platform\ISQLPlatform;
6
7/**
8 * A query builder for UNION queries takes SelectQueryBuilder objects
9 *
10 * Any particular query builder object should only be used for a single database query,
11 * and not be reused afterwards.
12 *
13 * @since 1.41
14 * @ingroup Database
15 */
16class UnionQueryBuilder {
17    /** sort the results in ascending order */
18    public const SORT_ASC = 'ASC';
19
20    /** sort the results in descending order */
21    public const SORT_DESC = 'DESC';
22
23    /**
24     * @var SelectQueryBuilder[]
25     */
26    private $sqbs = [];
27
28    /** @var IDatabase */
29    private $db;
30
31    /** @var bool */
32    private $all = IReadableDatabase::UNION_DISTINCT;
33
34    /** @var array */
35    private $options = [];
36
37    /**
38     * @var string The caller (function name) to be passed to IDatabase::query()
39     */
40    private $caller = __CLASS__;
41
42    /**
43     * To create a UnionQueryBuilder instance, use `$db->newUnionQueryBuilder()` instead.
44     *
45     * @param IDatabase $db
46     */
47    public function __construct( IDatabase $db ) {
48        $this->db = $db;
49    }
50
51    /**
52     * Add a select query builder object to the list of union
53     *
54     * @return $this
55     */
56    public function add( SelectQueryBuilder $selectQueryBuilder ) {
57        $this->sqbs[] = $selectQueryBuilder;
58        return $this;
59    }
60
61    /**
62     * Enable UNION_ALL option, the default is UNION_DISTINCT
63     *
64     * @return $this
65     */
66    public function all() {
67        $this->all = $this->db::UNION_ALL;
68        return $this;
69    }
70
71    /**
72     * Set the query limit. Return at most this many rows. The rows are sorted
73     * and then the first rows are taken until the limit is reached. Limit
74     * is applied to a result set after offset.
75     *
76     * If the query builder already has a limit, the old limit will be discarded.
77     * This would be also ignored if the DB does not support limit in union queries.
78     *
79     * @param int $limit
80     * @return $this
81     */
82    public function limit( $limit ) {
83        if ( !$this->db->unionSupportsOrderAndLimit() ) {
84            return $this;
85        }
86        $this->options['LIMIT'] = $limit;
87        return $this;
88    }
89
90    /**
91     * Set the offset. Skip this many rows at the start of the result set. Offset
92     * with limit() can theoretically be used for paging through a result set,
93     * but this is discouraged for performance reasons.
94     *
95     * If the query builder already has an offset, the old offset will be discarded.
96     * This would be also ignored if the DB does not support offset in union queries.
97     *
98     * @param int $offset
99     * @return $this
100     */
101    public function offset( $offset ) {
102        if ( !$this->db->unionSupportsOrderAndLimit() ) {
103            return $this;
104        }
105        $this->options['OFFSET'] = $offset;
106        return $this;
107    }
108
109    /**
110     * Set the ORDER BY clause. If it has already been set, append the
111     * additional fields to it.
112     *
113     * This would be ignored if the DB does not support order by in union queries.
114     *
115     * @param string[]|string $fields The field or list of fields to order by.
116     * @param-taint $fields exec_sql
117     * @param string|null $direction self::SORT_ASC or self::SORT_DESC.
118     * If this is null then $fields is assumed to optionally contain ASC or DESC
119     * after each field name.
120     * @param-taint $direction exec_sql
121     * @return $this
122     */
123    public function orderBy( $fields, $direction = null ) {
124        if ( !$this->db->unionSupportsOrderAndLimit() ) {
125            return $this;
126        }
127        if ( $direction === null ) {
128            $this->mergeOption( 'ORDER BY', $fields );
129        } elseif ( is_array( $fields ) ) {
130            $fieldsWithDirection = [];
131            foreach ( $fields as $field ) {
132                $fieldsWithDirection[] = "$field $direction";
133            }
134            $this->mergeOption( 'ORDER BY', $fieldsWithDirection );
135        } else {
136            $this->mergeOption( 'ORDER BY', "$fields $direction" );
137        }
138        return $this;
139    }
140
141    /**
142     * Add a value to an option which may be not set or a string or array.
143     *
144     * @param string $name
145     * @param string|string[] $newArrayOrValue
146     */
147    private function mergeOption( $name, $newArrayOrValue ) {
148        $value = isset( $this->options[$name] )
149            ? (array)$this->options[$name] : [];
150        if ( is_array( $newArrayOrValue ) ) {
151            $value = array_merge( $value, $newArrayOrValue );
152        } else {
153            $value[] = $newArrayOrValue;
154        }
155        $this->options[$name] = $value;
156    }
157
158    /**
159     * Set the method name to be included in an SQL comment.
160     *
161     * @param string $fname
162     * @param-taint $fname exec_sql
163     * @return $this
164     */
165    public function caller( $fname ) {
166        $this->caller = $fname;
167        return $this;
168    }
169
170    /**
171     * Run the constructed UNION query and return all results.
172     *
173     * @return IResultWrapper
174     */
175    public function fetchResultSet() {
176        $query = new Query( $this->getSQL(), ISQLPlatform::QUERY_CHANGE_NONE, 'SELECT' );
177        return $this->db->query( $query, $this->caller );
178    }
179
180    /**
181     * Run the constructed UNION query, and return a single field extracted
182     * from the first result row. If there were no result rows, false is
183     * returned. This may only be called when only one field has been added to
184     * the constituent queries.
185     *
186     * @since 1.42
187     * @return mixed
188     * @return-taint tainted
189     */
190    public function fetchField() {
191        $this->limit( 1 );
192        foreach ( $this->fetchResultSet() as $row ) {
193            $row = (array)$row;
194            if ( count( $row ) !== 1 ) {
195                throw new \UnexpectedValueException(
196                    __METHOD__ . ' expects the query to have only one field' );
197            }
198            return $row[ array_key_first( $row ) ];
199        }
200        return false;
201    }
202
203    /**
204     * Run the constructed UNION query, and extract a single field from each
205     * result row, returning an array containing all the values. This may only
206     * be called when only one field has been added to the constituent queries.
207     *
208     * @since 1.42
209     * @return array
210     * @return-taint tainted
211     */
212    public function fetchFieldValues() {
213        $values = [];
214        foreach ( $this->fetchResultSet() as $row ) {
215            $row = (array)$row;
216            if ( count( $row ) !== 1 ) {
217                throw new \UnexpectedValueException(
218                    __METHOD__ . ' expects the query to have only one field' );
219            }
220            $values[] = $row[ array_key_first( $row ) ];
221        }
222        return $values;
223    }
224
225    /**
226     * Run the constructed UNION query, and return the first result row. If
227     * there were no results, return false.
228     *
229     * @since 1.42
230     * @return \stdClass|false
231     * @return-taint tainted
232     */
233    public function fetchRow() {
234        $this->limit( 1 );
235        foreach ( $this->fetchResultSet() as $row ) {
236            return $row;
237        }
238        return false;
239    }
240
241    /**
242     * Get the SQL query string which would be used by fetchResultSet().
243     *
244     * @since 1.42
245     * @return string
246     */
247    public function getSQL() {
248        $sqls = [];
249        foreach ( $this->sqbs as $sqb ) {
250            $sqls[] = $sqb->getSQL();
251        }
252        return $this->db->unionQueries( $sqls, $this->all, $this->options );
253    }
254
255}