From a1c69842d253c0f531c611897d30db8801ba769f Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Sun, 31 May 2026 22:06:25 +0200 Subject: [PATCH] feat(database): add query builder insertGetID - add insertGetID() to Query Builder - return false when no insert result or no row is inserted - prevent Model from proxying the builder method - document the new API and cover core behavior with tests Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/Database/BaseBuilder.php | 20 ++++ system/Model.php | 1 + tests/system/Database/Builder/InsertTest.php | 105 ++++++++++++++++++ tests/system/Models/GetCompiledModelTest.php | 13 +++ user_guide_src/source/changelogs/v4.8.0.rst | 1 + .../source/database/query_builder.rst | 23 ++++ .../source/database/query_builder/130.php | 9 ++ 7 files changed, 172 insertions(+) create mode 100644 user_guide_src/source/database/query_builder/130.php diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 29af09d0f2f6..828f65db93c1 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2878,6 +2878,26 @@ public function insert($set = null, ?bool $escape = null) return false; } + /** + * Inserts a row and returns the insert ID. + * + * @param array|object|null $set + * + * @return false|int|string + * + * @throws DatabaseException + */ + public function insertGetID($set = null, ?bool $escape = null) + { + $result = $this->insert($set, $escape); + + if ($result === false || $result instanceof Query || $this->db->affectedRows() < 1) { + return false; + } + + return $this->db->insertID(); + } + /** * @internal This is a temporary solution. * diff --git a/system/Model.php b/system/Model.php index f6b125260398..48cee72f2fd8 100644 --- a/system/Model.php +++ b/system/Model.php @@ -162,6 +162,7 @@ class Model extends BaseModel 'getCompiledInsert', 'getCompiledSelect', 'getCompiledUpdate', + 'insertGetID', ]; public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null) diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index e8761b9de52b..4691e05f9f35 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Builder; +use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; use CodeIgniter\Database\RawSql; @@ -39,6 +40,51 @@ protected function setUp(): void $this->db = new MockConnection([]); } + private function insertGetIDConnection(int $affectedRows = 1, int $insertID = 123): MockConnection + { + return new class ($affectedRows, $insertID) extends MockConnection { + public function __construct( + private readonly int $affectedRowCount, + private readonly int $lastInsertID, + ) { + parent::__construct([]); + } + + /** + * @param mixed|null $binds + */ + public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = ''): bool|Query + { + $query = new Query($this); + $query->setQuery($sql, $binds, $setEscapeFlags); + + $this->lastQuery = $query; + + if ($this->pretend) { + return $query; + } + + $this->resultID = $this->simpleQuery($query->getQuery()); + + if ($this->resultID === false) { + return false; + } + + return $query->isWriteType(); + } + + public function affectedRows(): int + { + return $this->affectedRowCount; + } + + public function insertID(): int + { + return $this->lastInsertID; + } + }; + } + public function testInsertArray(): void { $builder = $this->db->table('jobs'); @@ -145,6 +191,65 @@ public function testThrowsExceptionOnNoValuesSet(): void $builder->testMode()->insert(null, true); } + public function testInsertGetIDReturnsInsertID(): void + { + $db = $this->insertGetIDConnection(insertID: 456); + $db->shouldReturn('execute', new class () {}); + + $insertID = (new BaseBuilder('jobs', $db))->insertGetID([ + 'name' => 'Grocery Sales', + ]); + + $this->assertSame(456, $insertID); + } + + public function testInsertGetIDReturnsFalseWhenInsertFails(): void + { + $db = $this->insertGetIDConnection(); + $db->shouldReturn('execute', false); + + $insertID = (new BaseBuilder('jobs', $db))->insertGetID([ + 'name' => 'Grocery Sales', + ]); + + $this->assertFalse($insertID); + } + + public function testInsertGetIDReturnsFalseWhenNoRowsAreInserted(): void + { + $db = $this->insertGetIDConnection(affectedRows: 0); + $db->shouldReturn('execute', new class () {}); + + $insertID = (new BaseBuilder('jobs', $db))->insertGetID([ + 'name' => 'Grocery Sales', + ]); + + $this->assertFalse($insertID); + } + + public function testInsertGetIDReturnsFalseInTestMode(): void + { + $db = $this->insertGetIDConnection(); + + $insertID = (new BaseBuilder('jobs', $db))->testMode()->insertGetID([ + 'name' => 'Grocery Sales', + ]); + + $this->assertFalse($insertID); + } + + public function testInsertGetIDReturnsFalseInPretendMode(): void + { + $db = $this->insertGetIDConnection(); + $db->pretend(); + + $insertID = (new BaseBuilder('jobs', $db))->insertGetID([ + 'name' => 'Grocery Sales', + ]); + + $this->assertFalse($insertID); + } + public function testInsertBatch(): void { $builder = $this->db->table('jobs'); diff --git a/tests/system/Models/GetCompiledModelTest.php b/tests/system/Models/GetCompiledModelTest.php index 77663bd49700..8a369a6aa17e 100644 --- a/tests/system/Models/GetCompiledModelTest.php +++ b/tests/system/Models/GetCompiledModelTest.php @@ -70,4 +70,17 @@ public function testGetCompiledUpdate(): void ->set('email', 'mark@example.com') ->getCompiledUpdate(); } + + public function testInsertGetID(): void + { + $this->expectException(ModelException::class); + $this->expectExceptionMessage('You cannot use "insertGetID()" in "Tests\Support\Models\UserObjModel".'); + + $this->createModel(UserObjModel::class); + + $this->model->insertGetID([ + 'name' => 'Mark', + 'email' => 'mark@example.com', + ]); + } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 359448aa83b5..f81d7a794bfb 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -221,6 +221,7 @@ Query Builder - Added ``exists()`` and ``doesntExist()`` to Query Builder to check whether the current Query Builder query would return at least one row. See :ref:`query-builder-exists`. - Added ``explain()`` to Query Builder to run execution-plan queries for the current ``SELECT`` query. See :ref:`query-builder-explain`. - Added ``havingBetween()``, ``orHavingBetween()``, ``havingNotBetween()``, and ``orHavingNotBetween()`` to Query Builder. See :ref:`query-builder-having-between`. +- Added ``insertGetID()`` to Query Builder to insert a row and return the insert ID reported by the database driver. See :ref:`query-builder-insert-get-id`. - 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`. diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 0144c28eeb65..cf490693729c 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1099,6 +1099,20 @@ Here is an example using an object: The first parameter is an object. +.. _query-builder-insert-get-id: + +$builder->insertGetID() +----------------------- + +.. versionadded:: 4.8.0 + +Inserts a row and returns the insert ID reported by the database driver: + +.. literalinclude:: query_builder/130.php + +This method returns ``false`` if the insert fails, no row is inserted, or the +builder is in test mode. It uses the same insert ID behavior as ``$db->insertID()``. + $builder->ignore() ------------------ @@ -2284,6 +2298,15 @@ Class Reference Compiles and executes an ``INSERT`` statement. + .. php:method:: insertGetID([$set = null[, $escape = null]]) + + :param array $set: An associative array of field/value pairs + :param bool $escape: Whether to escape values + :returns: The insert ID reported by the database driver, or ``false`` on failure + :rtype: int|string|false + + Compiles and executes an ``INSERT`` statement and returns the insert ID. + .. php:method:: insertBatch([$set = null[, $escape = null[, $batch_size = 100]]]) :param array $set: Data to insert diff --git a/user_guide_src/source/database/query_builder/130.php b/user_guide_src/source/database/query_builder/130.php new file mode 100644 index 000000000000..12ef4d362380 --- /dev/null +++ b/user_guide_src/source/database/query_builder/130.php @@ -0,0 +1,9 @@ + 'My title', + 'name' => 'My Name', + 'date' => '2022-01-01', +]; + +$insertID = $builder->insertGetID($data);