PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
← All changes | src/Framework/Models/ModelQueryBuilder.php +98 -0 4.16.0 → 4.18.0 View file →
@@ -4,9 +4,11 @@
4 4
5 5 use Give\Framework\Database\DB;
6 6 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
7 7 use Give\Framework\Models\Contracts\ModelCrud;
8 +use Give\Framework\QueryBuilder\Clauses\Having;
8 9 use Give\Framework\QueryBuilder\Clauses\RawSQL;
10 +use Give\Framework\QueryBuilder\Clauses\Select;
9 11 use Give\Framework\QueryBuilder\QueryBuilder;
10 12
11 13 /**
12 14 * @since 2.19.6
@@ -34,8 +36,11 @@
34 36
35 37 /**
36 38 * Returns the number of rows returned by a query
37 39 *
40 + * @since 4.18.0 Honor an explicit column in grouped counts by summing the column's non-null values per group.
41 + * @since 4.18.0 Preserve SELECT aliases referenced by HAVING when counting a grouped query.
42 + * @since 4.18.0 Count the groups of a grouped query rather than the first group's rows.
38 43 * @since 2.24.0
39 44 *
40 45 * @param null|string $column
41 46 */
@@ -42,8 +47,16 @@
42 47 public function count($column = null): int
43 48 {
44 49 $column = ( ! $column || $column === '*') ? '1' : trim($column);
45 50
51 + /*
52 + * A grouped query returns one row per group and get_row() reads only the first of them,
53 + * so the number of groups is what the caller is actually asking for.
54 + */
55 + if ($this->groupByColumns) {
56 + return +DB::get_row($this->getGroupCountSQL($column))->count;
57 + }
58 +
46 59 if ('1' === $column) {
47 60 $this->selects = [];
48 61 }
49 62 $this->selects[] = new RawSQL('SELECT COUNT(%1s) AS count', $column);
@@ -150,6 +163,91 @@
150 163
151 164 return array_map(static function ($object) use ($model) {
152 165 return $model::fromQueryBuilderObject($object);
153 166 }, $results);
167 + }
168 +
169 + /**
170 + * Wraps the grouped query so that its rows, one per group, are what gets counted. A
171 + * COUNT(DISTINCT ...) over the grouped columns would drop every group holding a NULL.
172 + *
173 + * An explicit column counts the column's non-null values, so each group contributes its
174 + * non-null count and the wrapper sums them instead of counting groups.
175 + *
176 + * The grouped columns are aliased because a derived table rejects duplicate column names, and
177 + * the ordering and paging are dropped because neither changes the number of groups. SELECT
178 + * entries whose aliases a HAVING clause references are kept, since the replaced select list
179 + * would otherwise leave HAVING pointing at an alias that no longer exists.
180 + *
181 + * @since 4.18.0
182 + *
183 + * @param string $column
184 + */
185 + private function getGroupCountSQL($column = null): string
186 + {
187 + $innerSelects = [];
188 +
189 + if ($column && '1' !== $column) {
190 + $innerSelects[] = DB::prepare('COUNT(%1s) AS nonNullCount', $column);
191 + }
192 +
193 + foreach ($this->groupByColumns as $index => $groupByColumn) {
194 + $innerSelects[] = "{$groupByColumn} AS groupedColumn{$index}";
195 + }
196 +
197 + foreach ($this->getHavingReferencedSelects() as $select) {
198 + $innerSelects[] = $select;
199 + }
200 +
201 + $this->selects = [new RawSQL('SELECT ' . implode(', ', $innerSelects))];
202 + $this->orderBys = [];
203 + $this->limit = null;
204 + $this->offset = null;
205 +
206 + if ($column && '1' !== $column) {
207 + return "SELECT SUM(nonNullCount) AS count FROM ({$this->getSQL()}) AS groupedQuery";
208 + }
209 +
210 + return "SELECT COUNT(*) AS count FROM ({$this->getSQL()}) AS groupedQuery";
211 + }
212 +
213 + /**
214 + * Renders the SELECT entries whose aliases a HAVING clause references, since the grouped
215 + * select list that count() builds would otherwise leave HAVING pointing at a missing alias.
216 + *
217 + * @since 4.18.0
218 + *
219 + * @return string[]
220 + */
221 + private function getHavingReferencedSelects(): array
222 + {
223 + $referencedAliases = [];
224 +
225 + foreach ($this->havings as $having) {
226 + if ($having instanceof Having) {
227 + $referencedAliases[] = $having->column;
228 + }
229 + }
230 +
231 + if ( ! $referencedAliases) {
232 + return [];
233 + }
234 +
235 + $selects = [];
236 +
237 + foreach ($this->selects as $select) {
238 + if ($select instanceof Select) {
239 + if (in_array($select->alias, $referencedAliases, true)) {
240 + $selects[] = DB::prepare('%1s AS %2s', $select->column, $select->alias);
241 + }
242 + } elseif ($select instanceof RawSQL) {
243 + if (preg_match('/\s+AS\s+([^\s,]+)\s*$/i', $select->sql, $matches) &&
244 + in_array(trim($matches[1], '`\'"'), $referencedAliases, true)
245 + ) {
246 + $selects[] = $select->sql;
247 + }
248 + }
249 + }
250 +
251 + return $selects;
154 252 }
155 253 }