diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 883c1aa7e5ec..b081f7c9a06a 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -19,6 +19,7 @@ use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Traits\ConditionalTrait; use Config\Feature; +use TypeError; /** * Class BaseBuilder @@ -2929,9 +2930,41 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri */ public function increment(string $column, int $value = 1) { - $column = $this->db->protectIdentifiers($column); + return $this->incrementMany([$column], $value); + } + + /** + * Increments multiple numeric columns by the specified value(s). + * + * @param array|list $columns A list of columns or array of column => value pairs to increment. + * @param int $value The value to increment by if $columns is a list of column names. + */ + public function incrementMany(array $columns, int $value = 1): bool + { + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } + + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + $fields[$col] = "{$col} + {$val}"; + } - $sql = $this->_update($this->QBFrom[0], [$column => "{$column} + {$value}"]); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); @@ -2949,9 +2982,41 @@ public function increment(string $column, int $value = 1) */ public function decrement(string $column, int $value = 1) { - $column = $this->db->protectIdentifiers($column); + return $this->decrementMany([$column], $value); + } + + /** + * Decrements multiple numeric columns by the specified value(s). + * + * @param array|list $columns A list of columns or array of column => value pairs to decrement. + * @param int $value The value to decrement by if $columns is a list of column names. + */ + public function decrementMany(array $columns, int $value = 1): bool + { + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } + + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + $fields[$col] = "{$col} - {$val}"; + } - $sql = $this->_update($this->QBFrom[0], [$column => "{$column}-{$value}"]); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 126ab5741892..5f5a6735ac96 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -17,6 +17,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; use CodeIgniter\Exceptions\InvalidArgumentException; +use TypeError; /** * Builder for Postgre @@ -87,17 +88,37 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = } /** - * Increments a numeric column by the specified value. + * Increments multiple numeric columns by the specified value(s). * - * @return mixed - * - * @throws DatabaseException + * @param array|list $columns A list of columns or array of column => value pairs to increment. + * @param int $value The value to increment by if $columns is a list of column names. */ - public function increment(string $column, int $value = 1) + public function incrementMany(array $columns, int $value = 1): bool { - $column = $this->db->protectIdentifiers($column); + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } + + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + $fields[$col] = "to_number({$col}, '9999999') + {$val}"; + } - $sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') + {$value}"]); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); @@ -109,17 +130,37 @@ public function increment(string $column, int $value = 1) } /** - * Decrements a numeric column by the specified value. + * Decrements multiple numeric columns by the specified value(s). * - * @return mixed - * - * @throws DatabaseException + * @param array|list $columns A list of columns or array of column => value pairs to decrement. + * @param int $value The value to decrement by if $columns is a list of column names. */ - public function decrement(string $column, int $value = 1) + public function decrementMany(array $columns, int $value = 1): bool { - $column = $this->db->protectIdentifiers($column); + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } + + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + $fields[$col] = "to_number({$col}, '9999999') - {$val}"; + } - $sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') - {$value}"]); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 96890bf45cb2..ffda2ba6cb08 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -18,7 +18,9 @@ use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\RawSql; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Feature; +use TypeError; /** * Builder for SQLSRV @@ -232,21 +234,41 @@ protected function _update(string $table, array $values): string } /** - * Increments a numeric column by the specified value. + * Increments multiple numeric columns by the specified value(s). * - * @return bool + * @param array|list $columns A list of columns or array of column => value pairs to increment. + * @param int $value The value to increment by if $columns is a list of column names. */ - public function increment(string $column, int $value = 1) + public function incrementMany(array $columns, int $value = 1): bool { - $column = $this->db->protectIdentifiers($column); + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } - if ($this->castTextToInt) { - $values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) + {$value})"]; - } else { - $values = [$column => "{$column} + {$value}"]; + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + if ($this->castTextToInt) { + $fields[$col] = "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$col})) + {$val})"; + } else { + $fields[$col] = "{$col} + {$val}"; + } } - $sql = $this->_update($this->QBFrom[0], $values); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); @@ -258,21 +280,41 @@ public function increment(string $column, int $value = 1) } /** - * Decrements a numeric column by the specified value. + * Decrements multiple numeric columns by the specified value(s). * - * @return bool + * @param array|list $columns A list of columns or array of column => value pairs to decrement. + * @param int $value The value to decrement by if $columns is a list of column names. */ - public function decrement(string $column, int $value = 1) + public function decrementMany(array $columns, int $value = 1): bool { - $column = $this->db->protectIdentifiers($column); + if ($columns === []) { + throw new InvalidArgumentException('Argument #1 ($columns) cannot be empty.'); + } - if ($this->castTextToInt) { - $values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) - {$value})"]; - } else { - $values = [$column => "{$column} + {$value}"]; + if (array_is_list($columns)) { + $columns = array_fill_keys($columns, $value); + } + + $fields = []; + + foreach ($columns as $col => $val) { + if (! is_int($val)) { + throw new TypeError(sprintf( + 'Argument #1 ($columns) must contain only int values, %s given for "%s".', + get_debug_type($val), + $col, + )); + } + + $col = $this->db->protectIdentifiers($col); + if ($this->castTextToInt) { + $fields[$col] = "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$col})) - {$val})"; + } else { + $fields[$col] = "{$col} - {$val}"; + } } - $sql = $this->_update($this->QBFrom[0], $values); + $sql = $this->_update($this->QBFrom[0], $fields); if (! $this->testMode) { $this->resetWrite(); diff --git a/tests/system/Database/Live/IncrementTest.php b/tests/system/Database/Live/IncrementTest.php index 0051e1a9039d..2c013002439d 100644 --- a/tests/system/Database/Live/IncrementTest.php +++ b/tests/system/Database/Live/IncrementTest.php @@ -13,10 +13,12 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Database\Seeds\CITestSeeder; +use TypeError; /** * @internal @@ -65,6 +67,77 @@ public function testResetStateAfterIncrement(): void $this->seeInDatabase('job', ['name' => 'account2', 'description' => '11']); } + public function testIncrementMany(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->incrementMany(['description' => 2, 'created_at' => 3]); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '8', 'created_at' => 4]); + } + + public function testIncrementManyWithValue(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->incrementMany(['description', 'created_at'], 2); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '8', 'created_at' => 3]); + } + + public function testIncrementManyWithNegativeValue(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->incrementMany(['description' => 2, 'created_at' => -1]); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '8', 'created_at' => 0]); + } + + public function testIncrementManyWithEmptyColumns(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument #1 ($columns) cannot be empty.'); + + $this->db->table('job') + ->where('name', 'task1') + ->incrementMany([]); + } + + public function testIncrementManyWithNonIntegerValues(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Argument #1 ($columns) must contain only int values, string given for "created_at".'); + + $this->db->table('job') + ->where('name', 'job1') + ->incrementMany(['description' => 2, 'created_at' => 'wrongValue']); + } + + public function testResetStateAfterIncrementMany(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + $this->hasInDatabase('job', ['name' => 'job2', 'description' => '2', 'created_at' => 4]); + + $builder = $this->db->table('job'); + + $builder->where('name', 'job1')->incrementMany(['description', 'created_at']); + $builder->where('name', 'job2')->incrementMany(['description', 'created_at']); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '7', 'created_at' => 2]); + $this->seeInDatabase('job', ['name' => 'job2', 'description' => '3', 'created_at' => 5]); + } + public function testDecrement(): void { $this->hasInDatabase('job', ['name' => 'incremental', 'description' => '6']); @@ -100,4 +173,75 @@ public function testResetStateAfterDecrement(): void $this->seeInDatabase('job', ['name' => 'account1', 'description' => '9']); $this->seeInDatabase('job', ['name' => 'account2', 'description' => '9']); } + + public function testDecrementMany(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->decrementMany(['description' => 2, 'created_at' => 3]); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '4', 'created_at' => -2]); + } + + public function testDecrementManyWithValue(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->decrementMany(['description', 'created_at'], 2); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '4', 'created_at' => -1]); + } + + public function testDecrementManyWithNegativeValues(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->db->table('job') + ->where('name', 'job1') + ->decrementMany(['description' => 2, 'created_at' => -1]); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '4', 'created_at' => 2]); + } + + public function testDecrementManyWithEmptyColumns(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Argument #1 ($columns) cannot be empty.'); + + $this->db->table('job') + ->where('name', 'task1') + ->decrementMany([]); + } + + public function testDecrementManyWithNonIntegerValues(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + + $this->expectException(TypeError::class); + $this->expectExceptionMessage('Argument #1 ($columns) must contain only int values, string given for "created_at".'); + + $this->db->table('job') + ->where('name', 'job1') + ->decrementMany(['description' => 2, 'created_at' => 'wrongValue']); + } + + public function testResetStateAfterDecrementMany(): void + { + $this->hasInDatabase('job', ['name' => 'job1', 'description' => '6', 'created_at' => 1]); + $this->hasInDatabase('job', ['name' => 'job2', 'description' => '2', 'created_at' => 4]); + + $builder = $this->db->table('job'); + + $builder->where('name', 'job1')->decrementMany(['description', 'created_at']); + $builder->where('name', 'job2')->decrementMany(['description', 'created_at']); + + $this->seeInDatabase('job', ['name' => 'job1', 'description' => '5', 'created_at' => 0]); + $this->seeInDatabase('job', ['name' => 'job2', 'description' => '1', 'created_at' => 3]); + } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index a9a33ee50165..f6ea71e9d702 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -200,6 +200,8 @@ Database Query Builder ------------- +- Added new ``incrementMany()`` and ``decrementMany()`` methods to ``CodeIgniter\Database\BaseBuilder`` for performing bulk increment/decrement operations. + Forge ----- diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 480b8c18d92e..5e0ae528e2a2 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -2040,15 +2040,41 @@ Class Reference is not a numeric field, like a ``VARCHAR``, it will likely be replaced with ``$value``. + .. php:method:: incrementMany($columns[, $value = 1]) + + .. versionadded:: 4.8.0 + + :param array $columns: A list of columns or array of column => value pairs to increment. + :param int $value: The amount to increment in the columns, if $columns is a list of columns. + :returns: ``true`` on success, ``false`` on failure + :rtype: bool + + Increments the value of multiple fields by the specified amounts. If a field + is not a numeric field, like a ``VARCHAR``, it will likely be replaced + with the amount specified for that field. + .. php:method:: decrement($column[, $value = 1]) :param string $column: The name of the column to decrement - :param int $value: The amount to decrement in the column + :param int $value: The amount to decrement in the column Decrements the value of a field by the specified amount. If the field is not a numeric field, like a ``VARCHAR``, it will likely be replaced with ``$value``. + .. php:method:: decrementMany($columns[, $value = 1]) + + .. versionadded:: 4.8.0 + + :param array $columns: A list of columns or array of column => value pairs to decrement. + :param int $value: The amount to decrement in the columns, if $columns is a list of columns. + :returns: ``true`` on success, ``false`` on failure + :rtype: bool + + Decrements the value of multiple fields by the specified amounts. If a field + is not a numeric field, like a ``VARCHAR``, it will likely be replaced + with the amount specified for that field. + .. php:method:: truncate() :returns: ``true`` on success, ``false`` on failure, string on test mode diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 60c71993ab4b..bf64e82e8883 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2036 errors +# total 2032 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon index cb796e2ba89f..fc3b3272ee8c 100644 --- a/utils/phpstan-baseline/method.childReturnType.neon +++ b/utils/phpstan-baseline/method.childReturnType.neon @@ -1,4 +1,4 @@ -# total 28 errors +# total 26 errors parameters: ignoreErrors: @@ -37,21 +37,11 @@ parameters: count: 1 path: ../../system/Database/Postgre/Builder.php - - - message: '#^Return type \(mixed\) of method CodeIgniter\\Database\\Postgre\\Builder\:\:decrement\(\) should be covariant with return type \(bool\) of method CodeIgniter\\Database\\BaseBuilder\:\:decrement\(\)$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - message: '#^Return type \(mixed\) of method CodeIgniter\\Database\\Postgre\\Builder\:\:delete\(\) should be covariant with return type \(bool\|string\) of method CodeIgniter\\Database\\BaseBuilder\:\:delete\(\)$#' count: 1 path: ../../system/Database/Postgre/Builder.php - - - message: '#^Return type \(mixed\) of method CodeIgniter\\Database\\Postgre\\Builder\:\:increment\(\) should be covariant with return type \(bool\) of method CodeIgniter\\Database\\BaseBuilder\:\:increment\(\)$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - message: '#^Return type \(mixed\) of method CodeIgniter\\Database\\Postgre\\Builder\:\:replace\(\) should be covariant with return type \(CodeIgniter\\Database\\BaseResult\|CodeIgniter\\Database\\Query\|string\|false\) of method CodeIgniter\\Database\\BaseBuilder\:\:replace\(\)$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index a8f562bbcc12..65cc1700d441 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1228 errors +# total 1226 errors parameters: ignoreErrors: