diff --git a/src/Database/IStructure.php b/src/Database/IStructure.php index 614a85a3e..8c5960845 100644 --- a/src/Database/IStructure.php +++ b/src/Database/IStructure.php @@ -46,9 +46,9 @@ function getColumns($table); function getPrimaryKey($table); /** - * Returns table primary key sequence. + * Returns table autoincrement primary key and sequence his name, if exists. * @param string - * @return string|NULL + * @return array|NULL */ function getPrimaryKeySequence($table); diff --git a/src/Database/Structure.php b/src/Database/Structure.php index dab408e9e..c83fcbf26 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -71,18 +71,21 @@ public function getPrimaryKeySequence($table) $this->needStructure(); $table = $this->resolveFQTableName($table); - if (!$this->connection->getSupplementalDriver()->isSupported(ISupplementalDriver::SUPPORT_SEQUENCE)) { - return NULL; - } - $primary = $this->getPrimaryKey($table); - if (!$primary || is_array($primary)) { + if (!$primary) { return NULL; } - foreach ($this->structure['columns'][$table] as $columnMeta) { - if ($columnMeta['name'] === $primary) { - return isset($columnMeta['vendor']['sequence']) ? $columnMeta['vendor']['sequence'] : NULL; + $isSupported = $this->connection->getSupplementalDriver()->isSupported(ISupplementalDriver::SUPPORT_SEQUENCE); + + foreach ((array) $primary as $key) { + foreach ($this->structure['columns'][$table] as $columnMeta) { + if ($columnMeta['name'] === $key && $columnMeta['autoincrement']) { + return [ + 'name' => $key, + 'sequence' => ($isSupported && isset($columnMeta['vendor']['sequence'])) ? $columnMeta['vendor']['sequence'] : NULL + ]; + } } } diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index ffe857e3c..c34371816 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -211,7 +211,7 @@ protected function emptyResultSet($saveCache = TRUE, $deleteRererencedCache = TR /********************* manipulation ****************d*g**/ - public function insert($data) + public function insert($data, $returnRow = TRUE) { if ($data instanceof \Traversable && !$data instanceof Selection) { $data = iterator_to_array($data); diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index 2349c3ac0..715d771c9 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -38,7 +38,7 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable /** @var string|array|NULL primary key field name */ protected $primary; - /** @var string|bool primary column sequence name, FALSE for autodetection */ + /** @var array|bool|NULL primary column sequence name, FALSE for autodetection */ protected $primarySequence = FALSE; /** @var IRow[] data read from database in [primary key => IRow] format */ @@ -136,7 +136,7 @@ public function getPrimary($need = TRUE) /** - * @return string + * @return array|NULL */ public function getPrimarySequence() { @@ -149,10 +149,10 @@ public function getPrimarySequence() /** - * @param string + * @param array * @return self */ - public function setPrimarySequence($sequence) + public function setPrimarySequence(array $sequence) { $this->primarySequence = $sequence; return $this; @@ -801,9 +801,10 @@ public function getDataRefreshed() /** * Inserts row in a table. * @param array|\Traversable|Selection array($column => $value)|\Traversable|Selection for INSERT ... SELECT - * @return IRow|int|bool Returns IRow or number of affected rows for Selection or table without primary key + * @param bool + * @return IRow|int|bool Returns IRow or number of affected rows for Selection, multi insert or table without primary key */ - public function insert($data) + public function insert($data, $returnRow = TRUE) { if ($data instanceof self) { $return = $this->context->queryArgs($this->sqlBuilder->buildInsertQuery() . ' ' . $data->getSql(), $data->getSqlBuilder()->getParameters()); @@ -817,36 +818,34 @@ public function insert($data) $this->loadRefCache(); - if ($data instanceof self || $this->primary === NULL) { - unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); - return $return->getRowCount(); - } - - $primaryKey = $this->context->getInsertId( - ($tmp = $this->getPrimarySequence()) - ? implode('.', array_map([$this->context->getConnection()->getSupplementalDriver(), 'delimite'], explode('.', $tmp))) - : NULL - ); - if ($primaryKey === FALSE) { + if ($data instanceof self || $this->primary === NULL || Nette\Utils\Arrays::isList($data) || !$returnRow) { unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); return $return->getRowCount(); } - if (is_array($this->getPrimary())) { - $primaryKey = []; - - foreach ((array) $this->getPrimary() as $key) { - if (!isset($data[$key])) { - return $data; - } - + $primaryKey = []; + foreach ((array) $this->getPrimary() as $key) { + if (isset($data[$key])) { $primaryKey[$key] = $data[$key]; } - if (count($primaryKey) === 1) { - $primaryKey = reset($primaryKey); + } + + if ($sequenceColumn = $this->getPrimarySequence()) { + $primaryKey[$sequenceColumn['name']] = $this->context->getInsertId( + ($sequenceColumn['sequence']) + ? implode('.', array_map([$this->context->getConnection()->getSupplementalDriver(), 'delimite'], explode('.', $sequenceColumn['sequence']))) + : NULL + ); + if ($primaryKey[$sequenceColumn['name']] === FALSE) { + unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); + return $return->getRowCount(); } } + if (count($primaryKey) === 1) { + $primaryKey = reset($primaryKey); + } + $row = $this->createSelectionInstance() ->select('*') ->wherePrimary($primaryKey) diff --git a/tests/Database/Structure.phpt b/tests/Database/Structure.phpt index c1298da7b..eb6a7f97f 100644 --- a/tests/Database/Structure.phpt +++ b/tests/Database/Structure.phpt @@ -58,24 +58,24 @@ class StructureTestCase extends TestCase ['name' => 'books_view', 'view' => TRUE], ]); $this->driver->shouldReceive('getColumns')->with('authors')->once()->andReturn([ - ['name' => 'id', 'primary' => TRUE, 'vendor' => ['sequence' => '"public"."authors_id_seq"']], - ['name' => 'name', 'primary' => FALSE, 'vendor' => []], + ['name' => 'id', 'primary' => TRUE, 'autoincrement' => TRUE, 'vendor' => ['sequence' => '"public"."authors_id_seq"']], + ['name' => 'name', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], ]); $this->driver->shouldReceive('getColumns')->with('Books')->once()->andReturn([ - ['name' => 'id', 'primary' => TRUE, 'vendor' => ['sequence' => '"public"."Books_id_seq"']], - ['name' => 'title', 'primary' => FALSE, 'vendor' => []], + ['name' => 'id', 'primary' => TRUE, 'autoincrement' => TRUE, 'vendor' => ['sequence' => '"public"."Books_id_seq"']], + ['name' => 'title', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], ]); $this->driver->shouldReceive('getColumns')->with('tags')->once()->andReturn([ - ['name' => 'id', 'primary' => TRUE, 'vendor' => []], - ['name' => 'name', 'primary' => FALSE, 'vendor' => []], + ['name' => 'id', 'primary' => TRUE, 'autoincrement' => TRUE, 'vendor' => []], + ['name' => 'name', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], ]); $this->driver->shouldReceive('getColumns')->with('books_x_tags')->once()->andReturn([ - ['name' => 'book_id', 'primary' => TRUE, 'vendor' => []], - ['name' => 'tag_id', 'primary' => TRUE, 'vendor' => []], + ['name' => 'book_id', 'primary' => TRUE, 'autoincrement' => FALSE, 'vendor' => []], + ['name' => 'tag_id', 'primary' => TRUE, 'autoincrement' => FALSE, 'vendor' => []], ]); $this->driver->shouldReceive('getColumns')->with('books_view')->once()->andReturn([ - ['name' => 'id', 'primary' => FALSE, 'vendor' => []], - ['name' => 'title', 'primary' => FALSE, 'vendor' => []], + ['name' => 'id', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], + ['name' => 'title', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], ]); $this->connection->shouldReceive('getSupplementalDriver')->times(4)->andReturn($this->driver); $this->driver->shouldReceive('getForeignKeys')->with('authors')->once()->andReturn([]); @@ -108,8 +108,8 @@ class StructureTestCase extends TestCase public function testGetColumns() { $columns = [ - ['name' => 'id', 'primary' => TRUE, 'vendor' => []], - ['name' => 'name', 'primary' => FALSE, 'vendor' => []], + ['name' => 'id', 'primary' => TRUE, 'autoincrement' => TRUE, 'vendor' => []], + ['name' => 'name', 'primary' => FALSE, 'autoincrement' => FALSE, 'vendor' => []], ]; Assert::same($columns, $this->structure->getColumns('tags')); @@ -138,9 +138,9 @@ class StructureTestCase extends TestCase $this->driver->shouldReceive('isSupported')->with('sequence')->once()->andReturn(FALSE); $this->driver->shouldReceive('isSupported')->with('sequence')->times(3)->andReturn(TRUE); - Assert::null($this->structure->getPrimaryKeySequence('Authors')); - Assert::same('"public"."authors_id_seq"', $this->structure->getPrimaryKeySequence('Authors')); - Assert::null($this->structure->getPrimaryKeySequence('tags')); + Assert::same(['name' => 'id', 'sequence' => NULL], $this->structure->getPrimaryKeySequence('Authors')); + Assert::same(['name' => 'id', 'sequence' => '"public"."authors_id_seq"'], $this->structure->getPrimaryKeySequence('Authors')); + Assert::same(['name' => 'id', 'sequence' => NULL] ,$this->structure->getPrimaryKeySequence('tags')); Assert::null($this->structure->getPrimaryKeySequence('books_x_tags')); } diff --git a/tests/Database/Table/Selection.insert().multi.phpt b/tests/Database/Table/Selection.insert().multi.phpt index c1ba63279..e33a29154 100644 --- a/tests/Database/Table/Selection.insert().multi.phpt +++ b/tests/Database/Table/Selection.insert().multi.phpt @@ -14,7 +14,7 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverN test(function () use ($context) { Assert::same(3, $context->table('author')->count()); - $context->table('author')->insert([ + $insert = $context->table('author')->insert([ [ 'name' => 'Catelyn Stark', 'web' => 'http://example.com', @@ -26,15 +26,17 @@ test(function () use ($context) { 'born' => new DateTime('2021-11-11'), ], ]); // INSERT INTO `author` (`name`, `web`, `born`) VALUES ('Catelyn Stark', 'http://example.com', '2011-11-11 00:00:00'), ('Sansa Stark', 'http://example.com', '2021-11-11 00:00:00') + Assert::same(2, $insert); Assert::same(5, $context->table('author')->count()); $context->table('book_tag')->where('book_id', 1)->delete(); // DELETE FROM `book_tag` WHERE (`book_id` = ?) Assert::same(4, $context->table('book_tag')->count()); - $context->table('book')->get(1)->related('book_tag')->insert([ // SELECT * FROM `book` WHERE (`id` = ?) + $insert = $context->table('book')->get(1)->related('book_tag')->insert([ // SELECT * FROM `book` WHERE (`id` = ?) ['tag_id' => 21], ['tag_id' => 22], ['tag_id' => 23], ]); // INSERT INTO `book_tag` (`tag_id`, `book_id`) VALUES (21, 1), (22, 1), (23, 1) + Assert::same(3, $insert); Assert::same(7, $context->table('book_tag')->count()); }); diff --git a/tests/Database/Table/Selection.insert().phpt b/tests/Database/Table/Selection.insert().phpt index 07fadc68d..c27a87e91 100644 --- a/tests/Database/Table/Selection.insert().phpt +++ b/tests/Database/Table/Selection.insert().phpt @@ -84,3 +84,9 @@ $inserted = $context->table('note')->insert([ 'note' => 'Good one!', ]); Assert::equal(1, $inserted); + +$affected = $context->table('note')->insert([ + 'book_id' => 2, + 'note' => 'Second one!', +], FALSE); +Assert::equal(1, $affected); diff --git a/tests/Database/Table/bugs/Selection.insert().phpt b/tests/Database/Table/bugs/Selection.insert().phpt new file mode 100644 index 000000000..4140ba091 --- /dev/null +++ b/tests/Database/Table/bugs/Selection.insert().phpt @@ -0,0 +1,51 @@ +table('simple_pk')->insert([ + 'id' => 8, + 'name' => 'Michal' + ]); + + Assert::equal(8, $inserted->id); + Assert::equal('Michal', $inserted->name); +}); + +//Insert into table with composite primary key +test(function () use ($context) { + + $inserted = $context->table('composite_pk')->insert([ + 'id1' => 8, + 'id2' => 10, + 'name' => 'Michal' + ]); + + Assert::equal(8, $inserted->id1); + Assert::equal(10, $inserted->id2); + Assert::equal('Michal', $inserted->name); +}); + +//Insert into table with composite primary key and one of them is auto_increment +test(function () use ($context, $driverName) { + + //Sqlite doesn't allow this type of table and sqlsrv's driver don't implement reflection + if ($driverName == 'mysql' || $driverName == 'pgsql') { + $inserted = $context->table('composite_pk_ai')->insert([ + 'id2' => 10, + 'name' => 'Michal' + ]); + + Assert::equal(10, $inserted->id2); + Assert::equal('Michal', $inserted->name); + } +}); diff --git a/tests/Database/Table/bugs/bug1342.postgre.phpt b/tests/Database/Table/bugs/bug1342.postgre.phpt index dcf7a5a21..f55b548ad 100644 --- a/tests/Database/Table/bugs/bug1342.postgre.phpt +++ b/tests/Database/Table/bugs/bug1342.postgre.phpt @@ -20,9 +20,10 @@ $context->query(' '); -$insertedRows = $context->table('bug1342')->insert([ +$row = $context->table('bug1342')->insert([ 'a1' => 1, 'a2' => 2, ]); -Assert::same(1, $insertedRows); +Assert::same(1, $row->a1); +Assert::same(2, $row->a2); diff --git a/tests/Database/files/mysql-nette_test4.sql b/tests/Database/files/mysql-nette_test4.sql new file mode 100644 index 000000000..0d9b55815 --- /dev/null +++ b/tests/Database/files/mysql-nette_test4.sql @@ -0,0 +1,26 @@ +/*!40102 SET storage_engine = InnoDB */; + +DROP DATABASE IF EXISTS nette_test; +CREATE DATABASE nette_test; +USE nette_test; + + +CREATE TABLE simple_pk ( + id int NOT NULL, + name varchar(100), + PRIMARY KEY (id) +); + +CREATE TABLE composite_pk ( + id1 int NOT NULL, + id2 int NOT NULL, + name varchar(100), + PRIMARY KEY (id1, id2) +); + +CREATE TABLE composite_pk_ai ( + id1 int NOT NULL AUTO_INCREMENT, + id2 int NOT NULL, + name varchar(100), + PRIMARY KEY (id1, id2) +); diff --git a/tests/Database/files/pgsql-nette_test4.sql b/tests/Database/files/pgsql-nette_test4.sql new file mode 100644 index 000000000..0118c234e --- /dev/null +++ b/tests/Database/files/pgsql-nette_test4.sql @@ -0,0 +1,22 @@ +DROP SCHEMA IF EXISTS public CASCADE; +CREATE SCHEMA public; + +CREATE TABLE simple_pk ( + id int NOT NULL, + name varchar(100), + PRIMARY KEY (id) +); + +CREATE TABLE composite_pk ( + id1 int NOT NULL, + id2 int NOT NULL, + name varchar(100), + PRIMARY KEY (id1, id2) +); + +CREATE TABLE composite_pk_ai ( + id1 serial NOT NULL, + id2 int NOT NULL, + name varchar(100), + PRIMARY KEY (id1, id2) +); diff --git a/tests/Database/files/sqlite-nette_test4.sql b/tests/Database/files/sqlite-nette_test4.sql new file mode 100644 index 000000000..e04f69245 --- /dev/null +++ b/tests/Database/files/sqlite-nette_test4.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS simple_pk; +DROP TABLE IF EXISTS composite_pk; + +CREATE TABLE simple_pk ( + id INT NOT NULL, + name TEXT, + PRIMARY KEY (id) +); + +CREATE TABLE composite_pk ( + id1 int NOT NULL, + id2 int NOT NULL, + name TEXT, + PRIMARY KEY (id1, id2) +); diff --git a/tests/Database/files/sqlsrv-nette_test4.sql.sql b/tests/Database/files/sqlsrv-nette_test4.sql.sql new file mode 100644 index 000000000..1bbd78e20 --- /dev/null +++ b/tests/Database/files/sqlsrv-nette_test4.sql.sql @@ -0,0 +1,23 @@ +IF OBJECT_ID('simple_pk', 'U') IS NOT NULL DROP TABLE simple_pk; +IF OBJECT_ID('composite_pk', 'U') IS NOT NULL DROP TABLE composite_pk; +IF OBJECT_ID('composite_pk_ai', 'U') IS NOT NULL DROP TABLE composite_pk_ai; + +CREATE TABLE simple_pk ( + id INTEGER NOT NULL, + name TEXT, + PRIMARY KEY (id) +); + +CREATE TABLE composite_pk ( + id1 INTEGER NOT NULL, + id2 INTEGER NOT NULL, + name TEXT, + PRIMARY KEY (id1, id2) +); + +CREATE TABLE composite_pk_ai ( + id1 INTEGER NOT NULL IDENTITY(1,1), + id2 INTEGER NOT NULL, + name TEXT, + PRIMARY KEY (id1, id2) +);