Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions system/Database/BaseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -947,11 +947,75 @@ public function orWhereNotBetween(?string $key = null, $values = null, ?bool $es
return $this->whereBetweenHaving('QBWhere', $key, $values, true, 'OR ', $escape);
}

/**
* Generates a HAVING field BETWEEN minimum AND maximum SQL query,
* joined with 'AND' if appropriate.
*
* @param array<array-key, mixed>|null $values The range values searched on
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function havingBetween(?string $key = null, $values = null, ?bool $escape = null): static
{
return $this->whereBetweenHaving('QBHaving', $key, $values, false, 'AND ', $escape);
}

/**
* Generates a HAVING field BETWEEN minimum AND maximum SQL query,
* joined with 'OR' if appropriate.
*
* @param array<array-key, mixed>|null $values The range values searched on
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function orHavingBetween(?string $key = null, $values = null, ?bool $escape = null): static
{
return $this->whereBetweenHaving('QBHaving', $key, $values, false, 'OR ', $escape);
}

/**
* Generates a HAVING field NOT BETWEEN minimum AND maximum SQL query,
* joined with 'AND' if appropriate.
*
* @param array<array-key, mixed>|null $values The range values searched on
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function havingNotBetween(?string $key = null, $values = null, ?bool $escape = null): static
{
return $this->whereBetweenHaving('QBHaving', $key, $values, true, 'AND ', $escape);
}

/**
* Generates a HAVING field NOT BETWEEN minimum AND maximum SQL query,
* joined with 'OR' if appropriate.
*
* @param array<array-key, mixed>|null $values The range values searched on
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function orHavingNotBetween(?string $key = null, $values = null, ?bool $escape = null): static
{
return $this->whereBetweenHaving('QBHaving', $key, $values, true, 'OR ', $escape);
}

/**
* @used-by whereBetween()
* @used-by orWhereBetween()
* @used-by whereNotBetween()
* @used-by orWhereNotBetween()
* @used-by havingBetween()
* @used-by orHavingBetween()
* @used-by havingNotBetween()
* @used-by orHavingNotBetween()
*
* @param 'QBHaving'|'QBWhere' $qbKey
* @param non-empty-string|null $key
Expand Down
4 changes: 4 additions & 0 deletions system/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@
* @method $this groupEnd()
* @method $this groupStart()
* @method $this having($key, $value = null, ?bool $escape = null)
* @method $this havingBetween(?string $key = null, $values = null, ?bool $escape = null)
* @method $this havingGroupEnd()
* @method $this havingGroupStart()
* @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null)
* @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
* @method $this havingNotBetween(?string $key = null, $values = null, ?bool $escape = null)
* @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
* @method $this join(string $table, string $cond, string $type = '', ?bool $escape = null)
* @method $this like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
Expand All @@ -62,9 +64,11 @@
* @method $this orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
* @method $this orGroupStart()
* @method $this orHaving($key, $value = null, ?bool $escape = null)
* @method $this orHavingBetween(?string $key = null, $values = null, ?bool $escape = null)
* @method $this orHavingGroupStart()
* @method $this orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
* @method $this orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
* @method $this orHavingNotBetween(?string $key = null, $values = null, ?bool $escape = null)
* @method $this orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
* @method $this orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
* @method $this orNotGroupStart()
Expand Down
131 changes: 131 additions & 0 deletions tests/system/Database/Builder/GroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
namespace CodeIgniter\Database\Builder;

use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockConnection;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;

/**
Expand Down Expand Up @@ -71,6 +73,135 @@ public function testOrHavingBy(): void
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

#[DataProvider('provideHavingBetweenMethods')]
public function testHavingBetweenMethods(string $method, string $sql): void
{
$builder = new BaseBuilder('user', $this->db);

$builder->select('name')
->groupBy('name')
->{$method}('total', [10, 20]);

$expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING "total" ' . $sql . ' 10 AND 20';
$expectedBinds = [
'total' => [
10,
true,
],
'total.1' => [
20,
true,
],
];

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
}

/**
* @return iterable<string, array{string, string}>
*/
public static function provideHavingBetweenMethods(): iterable
{
return [
'between' => ['havingBetween', 'BETWEEN'],
'not between' => ['havingNotBetween', 'NOT BETWEEN'],
];
}

#[DataProvider('provideOrHavingBetweenMethods')]
public function testOrHavingBetweenMethods(string $method, string $sql): void
{
$builder = new BaseBuilder('user', $this->db);

$builder->select('name')
->groupBy('name')
->having('active', 1)
->{$method}('total', [10, 20]);

$expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING "active" = 1 OR "total" ' . $sql . ' 10 AND 20';

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

/**
* @return iterable<string, array{string, string}>
*/
public static function provideOrHavingBetweenMethods(): iterable
{
return [
'or between' => ['orHavingBetween', 'BETWEEN'],
'or not between' => ['orHavingNotBetween', 'NOT BETWEEN'],
];
}

public function testHavingBetweenWithGroupedConditions(): void
{
$builder = new BaseBuilder('user', $this->db);

$builder->select('name')
->groupBy('name')
->havingGroupStart()
->havingBetween('total', [10, 20])
->orHavingNotBetween('score', [30, 40])
->havingGroupEnd()
->having('active', 1);

$expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING ( "total" BETWEEN 10 AND 20 OR "score" NOT BETWEEN 30 AND 40 ) AND "active" = 1';

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

public function testHavingBetweenNoEscape(): void
{
$builder = new BaseBuilder('user', $this->db);

$builder->select('name')
->groupBy('name')
->havingBetween('SUM(id)', [10, 20], escape: false);

$expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING SUM(id) BETWEEN 10 AND 20';
$expectedBinds = [
'SUM(id)' => [
10,
false,
],
'SUM(id).1' => [
20,
false,
],
];

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
}

/**
* @param mixed $values
*/
#[DataProvider('provideHavingBetweenInvalidValuesThrowInvalidArgumentException')]
public function testHavingBetweenInvalidValuesThrowInvalidArgumentException($values): void
{
$this->expectException(InvalidArgumentException::class);

$builder = new BaseBuilder('user', $this->db);
$builder->havingBetween('total', $values);
}

/**
* @return iterable<string, array{mixed}>
*/
public static function provideHavingBetweenInvalidValuesThrowInvalidArgumentException(): iterable
{
return [
'null' => [null],
'not array' => ['not array'],
'empty array' => [[]],
'one value' => [[10]],
'three values' => [[10, 20, 30]],
];
}

public function testHavingIn(): void
{
$builder = new BaseBuilder('user', $this->db);
Expand Down
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ Database
Query Builder
-------------

- Added ``havingBetween()``, ``orHavingBetween()``, ``havingNotBetween()``, and ``orHavingNotBetween()`` to Query Builder. See :ref:`query-builder-having-between`.
- Added ``whereBetween()``, ``orWhereBetween()``, ``whereNotBetween()``, and ``orWhereNotBetween()`` to Query Builder. See :ref:`query-builder-where-between`.
- Added ``whereColumn()`` and ``orWhereColumn()`` to compare one column to another column while protecting identifiers by default. See :ref:`query-builder-where-column`.
- Added ``whereExists()``, ``orWhereExists()``, ``whereNotExists()``, and ``orWhereNotExists()`` to add ``EXISTS`` and ``NOT EXISTS`` subquery conditions. See :ref:`query-builder-where-exists`.
Expand Down
78 changes: 78 additions & 0 deletions user_guide_src/source/database/query_builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,44 @@ $builder->orHaving()

Identical to ``having()``, only separates multiple clauses with **OR**.

.. _query-builder-having-between:

$builder->havingBetween()
-------------------------

.. versionadded:: 4.8.0

Generates a **HAVING** field ``BETWEEN`` minimum and maximum value SQL query.
``BETWEEN`` includes both values:

.. literalinclude:: query_builder/127.php

The range array must contain exactly two values: the lower and upper bounds.
These values are bound and escaped automatically. The ``$escape`` parameter
controls value escaping and identifier protection.

.. warning:: Do not pass user-supplied data as field names. If you need a more
complex SQL expression, use ``having()`` with :ref:`RawSql <query-builder-where-rawsql>`
and escape values manually.

$builder->orHavingBetween()
---------------------------

This method is identical to ``havingBetween()``, except that multiple instances
are joined by **OR**.

$builder->havingNotBetween()
----------------------------

This method is identical to ``havingBetween()``, except that it generates
``NOT BETWEEN``.

$builder->orHavingNotBetween()
------------------------------

This method is identical to ``havingNotBetween()``, except that multiple
instances are joined by **OR**.

$builder->havingIn()
--------------------

Expand Down Expand Up @@ -1932,6 +1970,46 @@ Class Reference

Adds a ``HAVING`` clause to a query, separating multiple calls with ``OR``.

.. php:method:: havingBetween([$key = null[, $values = null[, $escape = null]]])

:param string $key: Name of field to examine
:param array $values: Two values defining the inclusive range
:param bool $escape: Whether to escape values and protect identifiers
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Generates a ``HAVING`` field ``BETWEEN`` minimum and maximum value SQL query, joined with ``AND`` if appropriate.

.. php:method:: orHavingBetween([$key = null[, $values = null[, $escape = null]]])

:param string $key: The field to search
:param array $values: Two values defining the inclusive range
:param bool $escape: Whether to escape values and protect identifiers
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Generates a ``HAVING`` field ``BETWEEN`` minimum and maximum value SQL query, joined with ``OR`` if appropriate.

.. php:method:: havingNotBetween([$key = null[, $values = null[, $escape = null]]])

:param string $key: Name of field to examine
:param array $values: Two values defining the inclusive range
:param bool $escape: Whether to escape values and protect identifiers
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Generates a ``HAVING`` field ``NOT BETWEEN`` minimum and maximum value SQL query, joined with ``AND`` if appropriate.

.. php:method:: orHavingNotBetween([$key = null[, $values = null[, $escape = null]]])

:param string $key: The field to search
:param array $values: Two values defining the inclusive range
:param bool $escape: Whether to escape values and protect identifiers
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Generates a ``HAVING`` field ``NOT BETWEEN`` minimum and maximum value SQL query, joined with ``OR`` if appropriate.

.. php:method:: orHavingIn([$key = null[, $values = null[, $escape = null]]])

:param string $key: The field to search
Expand Down
6 changes: 6 additions & 0 deletions user_guide_src/source/database/query_builder/127.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

$builder->select('category')
->selectCount('id', 'total')
->groupBy('category')
->havingBetween('COUNT(id)', [10, 20], false);
Loading