├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .phive
└── phars.xml
├── LICENSE
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan-baseline.neon
├── phpstan.neon
├── phpunit.xml.dist
├── psalm.xml
├── src
└── Model
│ └── Behavior
│ ├── InsertBehavior.php
│ └── UpsertBehavior.php
└── tests
├── Fixture
├── ArticlesFixture.php
└── TagsFixture.php
├── TestCase
└── Model
│ └── Behavior
│ ├── InsertBehaviorTest.php
│ └── UpsertBehaviorTest.php
├── bootstrap.php
└── schema.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - 3.next
8 | pull_request:
9 | branches:
10 | - '*'
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | testsuite:
17 | uses: cakephp/.github/.github/workflows/testsuite-with-db.yml@5.x
18 | secrets: inherit
19 |
20 | cs-stan:
21 | uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x
22 | secrets: inherit
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /plugins
3 | /vendor
4 |
--------------------------------------------------------------------------------
/.phive/phars.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 itosho
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Easy Query
2 | CakePHP behavior plugin for easily generating some complicated queries like (bulk) insert/upsert etc.
3 |
4 | [](https://codecov.io/gh/itosho/easy-query)
5 | [](https://packagist.org/packages/itosho/easy-query)
6 | [](https://packagist.org/packages/itosho/easy-query)
7 | [](https://packagist.org/packages/itosho/easy-query)
8 |
9 | ## Requirements
10 | - PHP 8.1+
11 | - CakePHP 5.0+
12 | - MySQL 8.0+ / MariaDB 10.4+
13 |
14 | ### Notice
15 | - For CakePHP4.x, use 3.x tag.
16 | - For CakePHP3.x, use 1.x tag.
17 |
18 | ## Installation
19 | ```bash
20 | composer require itosho/easy-query
21 | ```
22 |
23 | ## Usage
24 |
25 | ### Upsert
26 | ```php
27 | $this->Tags = TableRegistry::getTableLocator()->get('Tags');
28 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
29 | 'uniqueColumns' => ['name'],
30 | 'updateColumns' => ['description', 'modified'],
31 | ]);
32 |
33 | $data = [
34 | 'name' => 'cakephp',
35 | 'description' => 'php web framework',
36 | ];
37 | $entity = $this->Tags->newEntity($data);
38 | $this->Tags->upsert($entity);
39 | ```
40 |
41 | ### Bulk Upsert
42 | ```php
43 | $this->Tags = TableRegistry::getTableLocator()->get('Tags');
44 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
45 | 'updateColumns' => ['description', 'modified'],
46 | ]);
47 |
48 | $data = [
49 | [
50 | 'name' => 'cakephp',
51 | 'description' => 'php web framework',
52 | ],
53 | [
54 | 'name' => 'rubyonrails',
55 | 'description' => 'ruby web framework',
56 | ]
57 | ];
58 | $entities = $this->Tags->newEntities($data);
59 | $this->Tags->bulkUpsert($entities);
60 | ```
61 |
62 | ### Bulk Insert
63 | ```php
64 | $this->Articles = TableRegistry::getTableLocator()->get('Articles');
65 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert');
66 |
67 | $data = [
68 | [
69 | 'title' => 'First Article',
70 | 'body' => 'First Article Body',
71 | 'published' => '1',
72 | ],
73 | [
74 | 'title' => 'Second Article',
75 | 'body' => 'Second Article Body',
76 | 'published' => '0',
77 | ]
78 | ];
79 | $entities = $this->Articles->newEntities($data);
80 | $this->Articles->bulkInsert($entities);
81 | ```
82 |
83 | ### Insert Select
84 | For inserting a record just once.
85 |
86 | #### case1
87 | Specify search conditions.
88 |
89 | ```php
90 | $this->Articles = TableRegistry::getTableLocator()->get('Articles');
91 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert');
92 |
93 | $data = [
94 | 'title' => 'New Article?',
95 | 'body' => 'New Article Body?',
96 | ];
97 | $entity = $this->Articles->newEntity($data);
98 | $condition = ['title' => 'New Article?'];
99 |
100 | $this->Articles->insertOnce($entities);
101 | ```
102 |
103 | Generated SQL is below.
104 |
105 | ```sql
106 | INSERT INTO articles (title, body)
107 | SELECT 'New Article?', 'New Article Body?' FROM tmp WHERE NOT EXISTS (
108 | SELECT * FROM articles WHERE title = 'New Article?'
109 | )
110 | ```
111 |
112 | #### case2
113 | Auto set search conditions with a inserting record.
114 |
115 | ```php
116 | $this->Articles = TableRegistry::getTableLocator()->get('Articles');
117 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert');
118 |
119 | $data = [
120 | 'title' => 'New Article',
121 | 'body' => 'New Article Body',
122 | ];
123 | $entity = $this->Articles->newEntity($data);
124 |
125 | $this->Articles->insertOnce($entities);
126 | ```
127 |
128 | Generated SQL is below.
129 |
130 | ```sql
131 | INSERT INTO articles (title, body)
132 | SELECT 'New Article', 'New Article Body' FROM tmp WHERE NOT EXISTS (
133 | SELECT * FROM articles WHERE title = 'New Article' AND body = 'New Article Body'
134 | )
135 | ```
136 |
137 | ### Advanced
138 | Need to use `Timestamp` behavior, if you want to update `created` and `modified` fields automatically.
139 | And you can change the action manually by using `event` config like this.
140 |
141 | ```php
142 | // default value is true
143 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert', [
144 | 'event' => ['beforeSave' => false],
145 | ]);
146 | ```
147 |
148 | ## Contributing
149 | Bug reports and pull requests are welcome on GitHub at https://github.com/itosho/easy-query.
150 |
151 | ## License
152 | The plugin is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
153 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "itosho/easy-query",
3 | "description": "CakePHP behavior plugin for easily generating some complicated queries like (bulk) insert/upsert etc.",
4 | "type": "cakephp-plugin",
5 | "keywords": [
6 | "cakephp",
7 | "behavior",
8 | "plugin",
9 | "upsert",
10 | "bulk upsert",
11 | "bulk insert",
12 | "insert select",
13 | "insert once"
14 | ],
15 | "homepage": "https://github.com/itosho/easy-query",
16 | "license": "MIT",
17 | "authors": [
18 | {
19 | "name": "itosho",
20 | "role": "Author",
21 | "homepage": "https://github.com/itosho"
22 | }
23 | ],
24 | "support": {
25 | "issues": "https://github.com/itosho/easy-query/issues",
26 | "source": "https://github.com/itosho/easy-query"
27 | },
28 | "require": {
29 | "php": ">=8.1",
30 | "cakephp/orm": "^5.0.0"
31 | },
32 | "require-dev": {
33 | "cakephp/cakephp": "^5.0.0",
34 | "phpunit/phpunit": "^10.1.0",
35 | "cakephp/cakephp-codesniffer": "^5.0",
36 | "vimeo/psalm": "^5.15"
37 | },
38 | "autoload": {
39 | "psr-4": {
40 | "Itosho\\EasyQuery\\": "src"
41 | }
42 | },
43 | "autoload-dev": {
44 | "psr-4": {
45 | "Itosho\\EasyQuery\\Test\\": "tests"
46 | }
47 | },
48 | "config": {
49 | "allow-plugins": {
50 | "dealerdirect/phpcodesniffer-composer-installer": true
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/phpstan-baseline.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors: []
3 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - phpstan-baseline.neon
3 |
4 | parameters:
5 | level: 6
6 | checkGenericClassInNonGenericObjectType: false
7 | checkMissingIterableValueType: false
8 | paths:
9 | - src/
10 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | tests/TestCase/
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | src/
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Model/Behavior/InsertBehavior.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | protected array $_defaultConfig = [
28 | 'event' => ['beforeSave' => true],
29 | ];
30 |
31 | /**
32 | * execute bulk insert query
33 | *
34 | * @param array<\Cake\Datasource\EntityInterface> $entities insert entities
35 | * @throws \LogicException no save data
36 | * @return \Cake\Database\StatementInterface query result
37 | */
38 | public function bulkInsert(array $entities): StatementInterface
39 | {
40 | $saveData = [];
41 | foreach ($entities as $entity) {
42 | if ($this->_config['event']['beforeSave']) {
43 | $this->_table->dispatchEvent('Model.beforeSave', compact('entity'));
44 | }
45 | $entity->setVirtual([]);
46 | array_push($saveData, $entity->toArray());
47 | }
48 |
49 | if (!isset($saveData[0])) {
50 | throw new LogicException('entities has no save data.');
51 | }
52 | $fields = array_keys($saveData[0]);
53 |
54 | $query = $this->_table
55 | ->insertQuery()
56 | ->insert($fields);
57 | $query->clause('values')->setValues($saveData);
58 |
59 | return $query->execute();
60 | }
61 |
62 | /**
63 | * execute insert select query for saving a record just once
64 | *
65 | * @param \Cake\Datasource\EntityInterface $entity insert entity
66 | * @param array|null $conditions search conditions
67 | * @return \Cake\Database\StatementInterface query result
68 | */
69 | public function insertOnce(EntityInterface $entity, ?array $conditions = null): StatementInterface
70 | {
71 | if ($this->_config['event']['beforeSave']) {
72 | $this->_table->dispatchEvent('Model.beforeSave', compact('entity'));
73 | }
74 |
75 | $entity->setVirtual([]);
76 | $insertData = $entity->toArray();
77 | if (isset($insertData['created'])) {
78 | $insertData['created'] = DateTime::now()->toDateTimeString();
79 | }
80 | if (isset($insertData['modified'])) {
81 | $insertData['modified'] = DateTime::now()->toDateTimeString();
82 | }
83 |
84 | $fields = array_keys($insertData);
85 | $existsConditions = $conditions;
86 | if (is_null($existsConditions)) {
87 | $existsConditions = $this->getExistsConditions($insertData);
88 | }
89 | $query = $this->_table->insertQuery()->insert($fields);
90 | $subQuery = $this
91 | ->buildTmpTableSelectQuery($insertData)
92 | ->where(function (QueryExpression $exp) use ($existsConditions) {
93 | $query = $this->_table
94 | ->find()
95 | ->where($existsConditions);
96 |
97 | return $exp->notExists($query);
98 | })
99 | ->limit(1);
100 |
101 | $query = $query->epilog($subQuery);
102 |
103 | return $query->execute();
104 | }
105 |
106 | /**
107 | * build tmp table's select query for insert select query
108 | *
109 | * @param array $insertData insert data
110 | * @return \Cake\ORM\Query\SelectQuery tmp table's select query
111 | * @throws \LogicException select query is invalid
112 | */
113 | private function buildTmpTableSelectQuery(array $insertData): SelectQuery
114 | {
115 | $driver = $this->_table
116 | ->getConnection()
117 | ->getDriver();
118 | $schema = [];
119 | $binds = [];
120 | foreach ($insertData as $key => $value) {
121 | $col = $driver->quoteIdentifier($key);
122 | if (is_null($value)) {
123 | $schema[] = "NULL AS $col";
124 | } else {
125 | $bindKey = ':' . strtolower($key);
126 | $binds[$bindKey] = $value;
127 | $schema[] = "$bindKey AS $col";
128 | }
129 | }
130 |
131 | $tmpTable = $this->fetchTable('tmp', [
132 | 'schema' => $this->_table->getSchema(),
133 | ]);
134 | $query = $tmpTable
135 | ->find()
136 | ->select(array_keys($insertData))
137 | ->from(
138 | sprintf('(SELECT %s) as tmp', implode(',', $schema))
139 | );
140 | $selectQuery = $query;
141 | foreach ($binds as $key => $value) {
142 | $selectQuery->bind($key, $value);
143 | }
144 |
145 | return $selectQuery;
146 | }
147 |
148 | /**
149 | * get conditions for finding a record already exists
150 | *
151 | * @param array $insertData insert data
152 | * @return array conditions
153 | */
154 | private function getExistsConditions(array $insertData): array
155 | {
156 | $autoFillFields = ['created', 'modified'];
157 | $existsConditions = [];
158 | foreach ($insertData as $field => $value) {
159 | if (in_array($field, $autoFillFields, true)) {
160 | continue;
161 | }
162 | $existsConditions[$field . ' IS'] = $value;
163 | }
164 |
165 | return $existsConditions;
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Model/Behavior/UpsertBehavior.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | protected array $_defaultConfig = [
22 | 'updateColumns' => null,
23 | 'uniqueColumns' => null,
24 | 'event' => ['beforeSave' => true],
25 | ];
26 |
27 | /**
28 | * execute upsert query
29 | *
30 | * @param \Cake\Datasource\EntityInterface $entity upsert entity
31 | * @return \Cake\Datasource\EntityInterface|array|null result entity
32 | * @throws \LogicException invalid config
33 | */
34 | public function upsert(EntityInterface $entity): array|EntityInterface|null
35 | {
36 | if (!$this->isValidArrayConfig('updateColumns')) {
37 | throw new LogicException('config updateColumns is invalid.');
38 | }
39 | if (!$this->isValidArrayConfig('uniqueColumns')) {
40 | throw new LogicException('config uniqueColumns is invalid.');
41 | }
42 |
43 | if ($this->_config['event']['beforeSave']) {
44 | $this->_table->dispatchEvent('Model.beforeSave', compact('entity'));
45 | }
46 | $entity->setVirtual([]);
47 | $upsertData = $entity->toArray();
48 | $fields = array_keys($upsertData);
49 |
50 | $updateColumns = $this->_config['updateColumns'];
51 |
52 | $updateValues = [];
53 | foreach ($updateColumns as $column) {
54 | $updateValues[] = "`$column`=VALUES(`$column`)";
55 | }
56 | $updateStatement = implode(', ', $updateValues);
57 | $expression = 'ON DUPLICATE KEY UPDATE ' . $updateStatement;
58 |
59 | $this->_table
60 | ->insertQuery()
61 | ->insert($fields)
62 | ->values($upsertData)
63 | ->epilog($expression)
64 | ->execute();
65 |
66 | $uniqueColumns = $this->_config['uniqueColumns'];
67 |
68 | $conditions = [];
69 | foreach ($uniqueColumns as $column) {
70 | $conditions[$column] = $upsertData[$column];
71 | }
72 |
73 | return $this->_table
74 | ->find()
75 | ->where($conditions)
76 | ->first();
77 | }
78 |
79 | /**
80 | * execute bulk upsert query
81 | *
82 | * @param array<\Cake\Datasource\EntityInterface> $entities upsert entities
83 | * @return \Cake\Database\StatementInterface query result
84 | * @throws \LogicException invalid config or no save data
85 | */
86 | public function bulkUpsert(array $entities): StatementInterface
87 | {
88 | if (!$this->isValidArrayConfig('updateColumns')) {
89 | throw new LogicException('config updateColumns is invalid.');
90 | }
91 |
92 | $saveData = [];
93 | foreach ($entities as $entity) {
94 | if ($this->_config['event']['beforeSave']) {
95 | $this->_table->dispatchEvent('Model.beforeSave', compact('entity'));
96 | }
97 | $entity->setVirtual([]);
98 | $saveData[] = $entity->toArray();
99 | }
100 |
101 | if (!isset($saveData[0])) {
102 | throw new LogicException('entities has no save data.');
103 | }
104 | $fields = array_keys($saveData[0]);
105 |
106 | $updateColumns = $this->_config['updateColumns'];
107 | $updateValues = [];
108 | foreach ($updateColumns as $column) {
109 | $updateValues[] = "`$column`=VALUES(`$column`)";
110 | }
111 | $updateStatement = implode(', ', $updateValues);
112 | $expression = 'ON DUPLICATE KEY UPDATE ' . $updateStatement;
113 | $query = $this->_table
114 | ->insertQuery()
115 | ->insert($fields)
116 | ->epilog($expression);
117 | $query->clause('values')->setValues($saveData);
118 |
119 | return $query->execute();
120 | }
121 |
122 | /**
123 | * validate config value
124 | *
125 | * @param string $configName config key
126 | * @return bool valid or invalid
127 | */
128 | private function isValidArrayConfig(string $configName): bool
129 | {
130 | $config = $this->_config[$configName];
131 |
132 | return is_array($config) && !empty($config);
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/tests/Fixture/ArticlesFixture.php:
--------------------------------------------------------------------------------
1 | 'First Article',
13 | 'body' => 'First Article Body',
14 | 'published' => 1,
15 | 'created' => '2017-09-01 00:00:00',
16 | 'modified' => '2017-09-01 00:00:00',
17 | ],
18 | [
19 | 'title' => 'Second Article',
20 | 'body' => 'Second Article Body',
21 | 'published' => 1,
22 | 'created' => '2017-09-01 00:00:00',
23 | 'modified' => '2017-09-01 00:00:00',
24 | ],
25 | [
26 | 'title' => 'Third Article',
27 | 'body' => 'Third Article Body',
28 | 'published' => 1,
29 | 'created' => '2017-09-01 00:00:00',
30 | 'modified' => '2017-09-01 00:00:00',
31 | ],
32 | ];
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Fixture/TagsFixture.php:
--------------------------------------------------------------------------------
1 | 'tag1',
13 | 'description' => 'tag1 description',
14 | 'created' => '2017-09-01 00:00:00',
15 | 'modified' => '2017-09-01 00:00:00',
16 | ],
17 | [
18 | 'name' => 'tag2',
19 | 'description' => 'tag2 description',
20 | 'created' => '2017-09-01 00:00:00',
21 | 'modified' => '2017-09-01 00:00:00',
22 | ],
23 | [
24 | 'name' => 'tag3',
25 | 'description' => 'tag3 description',
26 | 'created' => '2017-09-01 00:00:00',
27 | 'modified' => '2017-09-01 00:00:00',
28 | ],
29 | ];
30 | }
31 |
--------------------------------------------------------------------------------
/tests/TestCase/Model/Behavior/InsertBehaviorTest.php:
--------------------------------------------------------------------------------
1 | Articles = $this->getTableLocator()->get('Itosho/EasyQuery.Articles');
36 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert');
37 | }
38 |
39 | /**
40 | * @inheritDoc
41 | */
42 | public function tearDown(): void
43 | {
44 | parent::tearDown();
45 | $this->getTableLocator()->clear();
46 | unset($this->Articles);
47 | }
48 |
49 | /**
50 | * bulkInsert() test
51 | *
52 | * @return void
53 | */
54 | public function testBulkInsert()
55 | {
56 | $records = $this->getBaseInsertRecords();
57 | $now = DateTime::now();
58 | foreach ($records as $key => $val) {
59 | $record[$key]['created'] = $now;
60 | $record[$key]['modified'] = $now;
61 | }
62 |
63 | $entities = $this->Articles->newEntities($records);
64 | $this->Articles->bulkInsert($entities);
65 |
66 | foreach ($records as $conditions) {
67 | $actual = $this->Articles->exists($conditions);
68 | $this->assertTrue($actual, 'fail insert.');
69 | }
70 | }
71 |
72 | /**
73 | * bulkInsert() test add timestamp behavior
74 | *
75 | * @return void
76 | */
77 | public function testBulkInsertAddTimestamp()
78 | {
79 | $this->Articles->addBehavior('Timestamp');
80 |
81 | $records = $this->getBaseInsertRecords();
82 | $customNow = '2017-01-01 00:00:00';
83 | $records[0]['created'] = $customNow;
84 | $records[0]['modified'] = $customNow;
85 |
86 | $expectedRecords = $this->getBaseInsertRecords();
87 | $now = DateTime::now();
88 | foreach ($expectedRecords as $key => $val) {
89 | $expectedRecords[$key]['created'] = $now;
90 | $expectedRecords[$key]['modified'] = $now;
91 | }
92 | $expectedRecords[0]['created'] = $customNow;
93 | $expectedRecords[0]['modified'] = $customNow;
94 |
95 | $entities = $this->Articles->newEntities($records);
96 | $this->Articles->bulkInsert($entities);
97 |
98 | foreach ($expectedRecords as $conditions) {
99 | $actual = $this->Articles->exists($conditions);
100 | $this->assertTrue($actual, 'fail insert.');
101 | }
102 | }
103 |
104 | /**
105 | * bulkInsert() test beforeSave not dispatched
106 | *
107 | * @return void
108 | */
109 | public function testBulkInsertNoBeforeSave()
110 | {
111 | $this->Articles->removeBehavior('Insert');
112 | $this->Articles->addBehavior('Itosho/EasyQuery.Insert', [
113 | 'event' => ['beforeSave' => false],
114 | ]);
115 |
116 | $records = $this->getBaseInsertRecords();
117 | $customNow = '2017-01-01 00:00:00';
118 | $records[0]['created'] = $customNow;
119 | $records[0]['modified'] = $customNow;
120 |
121 | $expectedRecords = $this->getBaseInsertRecords();
122 | foreach ($expectedRecords as $key => $val) {
123 | $expectedRecords[$key]['created IS'] = null;
124 | $expectedRecords[$key]['modified IS'] = null;
125 | }
126 | unset($expectedRecords[0]['created IS']);
127 | unset($expectedRecords[0]['modified IS']);
128 | $expectedRecords[0]['created'] = $customNow;
129 | $expectedRecords[0]['modified'] = $customNow;
130 |
131 | $entities = $this->Articles->newEntities($records);
132 | $this->Articles->bulkInsert($entities);
133 |
134 | foreach ($expectedRecords as $conditions) {
135 | $actual = $this->Articles->exists($conditions);
136 | $this->assertTrue($actual, 'fail insert.');
137 | }
138 | }
139 |
140 | /**
141 | * bulkInsert() test by no data
142 | *
143 | * @return void
144 | */
145 | public function testBulkInsertNoSaveData()
146 | {
147 | $this->expectExceptionMessage('entities has no save data.');
148 | $this->expectException(LogicException::class);
149 |
150 | $this->Articles->bulkInsert([]);
151 | }
152 |
153 | /**
154 | * insertOnce() test
155 | *
156 | * @return void
157 | */
158 | public function testInsertOnce()
159 | {
160 | $newData = [
161 | 'title' => 'New Article',
162 | 'body' => 'New Article Body',
163 | 'published' => 1,
164 | ];
165 | $entity = $this->Articles->newEntity($newData);
166 |
167 | $this->Articles->insertOnce($entity);
168 |
169 | $actual = $this->Articles
170 | ->find()
171 | ->where($newData)
172 | ->all();
173 |
174 | $this->assertCount(1, $actual, 'fail insert once.');
175 | }
176 |
177 | /**
178 | * insertOnce() test add timestamp behavior
179 | *
180 | * @return void
181 | */
182 | public function testInsertOnceAddTimestampBehavior()
183 | {
184 | $this->Articles->addBehavior('Timestamp');
185 |
186 | $newData = [
187 | 'title' => 'New Article',
188 | 'body' => 'New Article Body',
189 | 'published' => 1,
190 | ];
191 | $entity = $this->Articles->newEntity($newData);
192 | $now = DateTime::now();
193 |
194 | $this->Articles->insertOnce($entity);
195 |
196 | $newData['created'] = $now;
197 | $newData['modified'] = $now;
198 |
199 | $actual = $this->Articles->exists($newData);
200 | $this->assertTrue($actual, 'fail insert.');
201 | }
202 |
203 | /**
204 | * insertOnce() test when duplicated
205 | *
206 | * @return void
207 | */
208 | public function testInsertOnceWhenDuplicated()
209 | {
210 | $duplicatedData = [
211 | 'title' => 'First Article',
212 | 'body' => 'First Article Body',
213 | 'published' => 1,
214 | ];
215 | $entity = $this->Articles->newEntity($duplicatedData);
216 |
217 | $this->Articles->insertOnce($entity);
218 |
219 | $actual = $this->Articles
220 | ->find()
221 | ->where($duplicatedData)
222 | ->all();
223 |
224 | $this->assertCount(1, $actual, 'fail insert once.');
225 | }
226 |
227 | /**
228 | * insertOnce() test when is null
229 | *
230 | * @return void
231 | */
232 | public function testInsertOnceWhenIsNull()
233 | {
234 | $newData = [
235 | 'title' => 'First Article',
236 | 'body' => null,
237 | 'published' => 1,
238 | ];
239 | $entity = $this->Articles->newEntity($newData);
240 |
241 | $this->Articles->insertOnce($entity);
242 |
243 | $actual = $this->Articles
244 | ->find()
245 | ->where([
246 | 'title' => 'First Article',
247 | 'body IS' => null,
248 | 'published' => 1,
249 | ])
250 | ->all();
251 |
252 | $this->assertCount(1, $actual, 'fail insert once.');
253 | }
254 |
255 | /**
256 | * insertOnce() test with conditions
257 | *
258 | * @return void
259 | */
260 | public function testInsertOnceWithConditions()
261 | {
262 | $newData = [
263 | 'title' => 'First Article',
264 | 'body' => 'First Article Body',
265 | 'published' => 0,
266 | ];
267 | $entity = $this->Articles->newEntity($newData);
268 |
269 | $conditions = [
270 | 'title' => 'Brand New First Article',
271 | 'body' => 'Brand New First Article Body',
272 | ];
273 |
274 | $this->Articles->insertOnce($entity, $conditions);
275 |
276 | $actual = $this->Articles
277 | ->find()
278 | ->where([
279 | 'title' => 'First Article',
280 | 'body' => 'First Article Body',
281 | ])
282 | ->all();
283 |
284 | $this->assertCount(2, $actual, 'fail insert once.');
285 | }
286 |
287 | /**
288 | * insertOnce() test when duplicated with conditions
289 | *
290 | * @return void
291 | */
292 | public function testInsertOnceWhenDuplicatedWithConditions()
293 | {
294 | $newData = [
295 | 'title' => 'First Article',
296 | 'body' => 'First Article Body',
297 | 'published' => 0,
298 | ];
299 | $entity = $this->Articles->newEntity($newData);
300 |
301 | $conditions = [
302 | 'title' => 'First Article',
303 | 'body' => 'First Article Body',
304 | ];
305 |
306 | $this->Articles->insertOnce($entity, $conditions);
307 |
308 | $actual = $this->Articles
309 | ->find()
310 | ->where($conditions)
311 | ->all();
312 |
313 | $this->assertCount(1, $actual, 'fail insert once.');
314 | }
315 |
316 | /**
317 | * get base insert records
318 | *
319 | * @return array
320 | */
321 | private function getBaseInsertRecords(): array
322 | {
323 | return [
324 | [
325 | 'title' => 'Fourth Article',
326 | 'body' => 'Fourth Article Body',
327 | 'published' => 1,
328 | ],
329 | [
330 | 'title' => 'Fifth Article',
331 | 'body' => 'Fifth Article Body',
332 | 'published' => 1,
333 | ],
334 | [
335 | 'title' => 'Sixth Article',
336 | 'body' => 'Sixth Article Body',
337 | 'published' => 1,
338 | ],
339 | ];
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/tests/TestCase/Model/Behavior/UpsertBehaviorTest.php:
--------------------------------------------------------------------------------
1 | Tags = $this->getTableLocator()->get('Itosho/EasyQuery.Tags');
36 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
37 | 'uniqueColumns' => ['name'],
38 | 'updateColumns' => ['description', 'modified'],
39 | ]);
40 | }
41 |
42 | /**
43 | * @inheritDoc
44 | */
45 | public function tearDown(): void
46 | {
47 | parent::tearDown();
48 | $this->getTableLocator()->clear();
49 | unset($this->Tags);
50 | }
51 |
52 | /**
53 | * upsert() test by insert
54 | *
55 | * @return void
56 | */
57 | public function testUpsertByInsert()
58 | {
59 | $now = DateTime::now();
60 | $record = [
61 | 'name' => 'tag4',
62 | 'description' => 'tag4 description',
63 | 'created' => $now,
64 | 'modified' => $now,
65 | ];
66 | $entity = $this->Tags->newEntity($record);
67 | $actual = $this->Tags->upsert($entity);
68 |
69 | $this->assertTrue($this->Tags->exists($record), 'fail insert.');
70 |
71 | $insertId = 4;
72 | $this->assertSame($insertId, $actual->id, 'return invalid id.');
73 | $this->assertSame($entity->name, $actual->name, 'return invalid name.');
74 | $this->assertSame($entity->description, $actual->description, 'return invalid description.');
75 | $this->assertSame(
76 | $entity->created->toDateTimeString(),
77 | $actual->created->toDateTimeString(),
78 | 'return invalid created.'
79 | );
80 | $this->assertSame(
81 | $entity->modified->toDateTimeString(),
82 | $actual->modified->toDateTimeString(),
83 | 'return invalid modified.'
84 | );
85 | }
86 |
87 | /**
88 | * upsert() test by insert add timestamp behavior
89 | *
90 | * @return void
91 | */
92 | public function testUpsertByInsertAddTimestamp()
93 | {
94 | $this->Tags->addBehavior('Timestamp');
95 |
96 | $record = [
97 | 'name' => 'tag4',
98 | 'description' => 'tag4 description',
99 | ];
100 | $now = DateTime::now();
101 | $expectedRecord = $record;
102 | $expectedRecord['created'] = $now;
103 | $expectedRecord['modified'] = $now;
104 |
105 | $entity = $this->Tags->newEntity($record);
106 | $actual = $this->Tags->upsert($entity);
107 |
108 | $this->assertTrue($this->Tags->exists($expectedRecord), 'fail insert.');
109 |
110 | $insertId = 4;
111 | $this->assertSame($insertId, $actual->id, 'return invalid id.');
112 | $this->assertSame($entity->name, $actual->name, 'return invalid name.');
113 | $this->assertSame($entity->description, $actual->description, 'return invalid description.');
114 | $this->assertSame(
115 | $entity->created->toDateTimeString(),
116 | $actual->created->toDateTimeString(),
117 | 'return invalid created.'
118 | );
119 | $this->assertSame(
120 | $entity->modified->toDateTimeString(),
121 | $actual->modified->toDateTimeString(),
122 | 'return invalid modified.'
123 | );
124 | }
125 |
126 | /**
127 | * upsert() test by update
128 | *
129 | * @return void
130 | */
131 | public function testUpsertByUpdate()
132 | {
133 | $record = [
134 | 'name' => 'tag1',
135 | 'description' => 'brand new tag1 description',
136 | 'created' => '2017-10-01 00:00:00',
137 | 'modified' => '2017-10-01 00:00:00',
138 | ];
139 | $entity = $this->Tags->newEntity($record);
140 | $actual = $this->Tags->upsert($entity);
141 | $currentCreated = '2017-09-01 00:00:00';
142 |
143 | $record['created'] = $currentCreated;
144 | $this->assertTrue($this->Tags->exists($record), 'fail update.');
145 |
146 | $updateId = 1;
147 | $this->assertSame($updateId, $actual->id, 'return invalid id.');
148 | $this->assertSame($entity->name, $actual->name, 'return invalid name.');
149 | $this->assertSame($entity->description, $actual->description, 'return invalid description.');
150 | $this->assertSame(
151 | $currentCreated,
152 | $actual->created->toDateTimeString(),
153 | 'return invalid created.'
154 | );
155 | $this->assertSame(
156 | $entity->modified->toDateTimeString(),
157 | $actual->modified->toDateTimeString(),
158 | 'return invalid modified.'
159 | );
160 | }
161 |
162 | /**
163 | * upsert() test by update add timestamp behavior
164 | *
165 | * @return void
166 | */
167 | public function testUpsertByUpdateAddTimestamp()
168 | {
169 | $this->Tags->addBehavior('Timestamp');
170 |
171 | $record = [
172 | 'name' => 'tag1',
173 | 'description' => 'brand new tag1 description',
174 | ];
175 | $now = DateTime::now();
176 | $currentCreated = '2017-09-01 00:00:00';
177 | $expectedRecord = $record;
178 | $expectedRecord['created'] = $currentCreated;
179 | $expectedRecord['modified'] = $now;
180 |
181 | $entity = $this->Tags->newEntity($record);
182 | $actual = $this->Tags->upsert($entity);
183 |
184 | $this->assertTrue($this->Tags->exists($expectedRecord), 'fail update.');
185 |
186 | $updateId = 1;
187 | $this->assertSame($updateId, $actual->id, 'return invalid id.');
188 | $this->assertSame($entity->name, $actual->name, 'return invalid name.');
189 | $this->assertSame($entity->description, $actual->description, 'return invalid description.');
190 | $this->assertSame(
191 | $currentCreated,
192 | $actual->created->toDateTimeString(),
193 | 'return invalid created.'
194 | );
195 | $this->assertSame(
196 | $entity->modified->toDateTimeString(),
197 | $actual->modified->toDateTimeString(),
198 | 'return invalid modified.'
199 | );
200 | }
201 |
202 | /**
203 | * upsert() test beforeSave not dispatched
204 | *
205 | * @return void
206 | */
207 | public function testUpsertNoBeforeSave()
208 | {
209 | $this->Tags->removeBehavior('Upsert');
210 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
211 | 'uniqueColumns' => ['name'],
212 | 'updateColumns' => ['description', 'modified'],
213 | 'event' => ['beforeSave' => false],
214 | ]);
215 | $this->Tags->addBehavior('Timestamp');
216 |
217 | $record = [
218 | 'name' => 'tag4',
219 | 'description' => 'tag4 description',
220 | ];
221 | $expectedRecord = $record;
222 | $expectedRecord['created IS'] = null;
223 | $expectedRecord['modified IS'] = null;
224 |
225 | $entity = $this->Tags->newEntity($record);
226 | $actual = $this->Tags->upsert($entity);
227 |
228 | $this->assertTrue($this->Tags->exists($expectedRecord), 'fail insert.');
229 |
230 | $insertId = 4;
231 | $this->assertSame($insertId, $actual->id, 'return invalid id.');
232 | $this->assertSame($entity->name, $actual->name, 'return invalid name.');
233 | $this->assertSame($entity->description, $actual->description, 'return invalid description.');
234 | $this->assertSame($entity->created, $actual->created, 'return invalid created.');
235 | $this->assertSame($entity->modified, $actual->modified, 'return invalid modified.');
236 | }
237 |
238 | /**
239 | * upsert() test when invalid update columns
240 | *
241 | * @return void
242 | */
243 | public function testUpsertInvalidUpdateColumnsConfig()
244 | {
245 | $this->expectExceptionMessage('config updateColumns is invalid.');
246 | $this->expectException(LogicException::class);
247 |
248 | $this->Tags->removeBehavior('Upsert');
249 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
250 | 'uniqueColumns' => ['name'],
251 | ]);
252 |
253 | $data = [
254 | 'name' => 'tag4',
255 | 'description' => 'tag4 description',
256 | 'created' => '2017-09-01 00:00:00',
257 | 'modified' => '2017-09-01 00:00:00',
258 | ];
259 | $entity = $this->Tags->newEntity($data);
260 | $this->Tags->upsert($entity);
261 | }
262 |
263 | /**
264 | * upsert() test when invalid unique columns
265 | *
266 | * @return void
267 | */
268 | public function testUpsertInvalidUniqueColumnsConfig()
269 | {
270 | $this->expectExceptionMessage('config uniqueColumns is invalid.');
271 | $this->expectException(LogicException::class);
272 |
273 | $this->Tags->removeBehavior('Upsert');
274 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
275 | 'updateColumns' => ['description', 'modified'],
276 | ]);
277 |
278 | $data = [
279 | 'name' => 'tag4',
280 | 'description' => 'tag4 description',
281 | 'created' => '2017-09-01 00:00:00',
282 | 'modified' => '2017-09-01 00:00:00',
283 | ];
284 | $entity = $this->Tags->newEntity($data);
285 | $this->Tags->upsert($entity);
286 | }
287 |
288 | /**
289 | * bulkUpsert() test by insert
290 | *
291 | * @return void
292 | */
293 | public function testBulkUpsertByInsert()
294 | {
295 | $this->Tags->removeBehavior('Upsert');
296 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
297 | 'updateColumns' => ['description', 'modified'],
298 | ]);
299 |
300 | $records = $this->getBaseInsertRecords();
301 | $now = DateTime::now();
302 | foreach ($records as $key => $val) {
303 | $records[$key]['created'] = $now;
304 | $records[$key]['modified'] = $now;
305 | }
306 |
307 | $entities = $this->Tags->newEntities($records);
308 | $this->Tags->bulkUpsert($entities);
309 |
310 | foreach ($records as $conditions) {
311 | $actual = $this->Tags->exists($conditions);
312 | $this->assertTrue($actual, 'fail insert.');
313 | }
314 | }
315 |
316 | /**
317 | * bulkUpsert() test by insert add timestamp behavior
318 | *
319 | * @return void
320 | */
321 | public function testBulkUpsertByInsertAddTimestamp()
322 | {
323 | $this->Tags->removeBehavior('Upsert');
324 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
325 | 'updateColumns' => ['description', 'modified'],
326 | ]);
327 | $this->Tags->addBehavior('Timestamp');
328 |
329 | $records = $this->getBaseInsertRecords();
330 | $now = DateTime::now();
331 | $expectedRecords = $records;
332 | foreach ($expectedRecords as $key => $val) {
333 | $expectedRecords[$key]['created'] = $now;
334 | $expectedRecords[$key]['modified'] = $now;
335 | }
336 |
337 | $entities = $this->Tags->newEntities($records);
338 | $this->Tags->bulkUpsert($entities);
339 |
340 | foreach ($expectedRecords as $conditions) {
341 | $actual = $this->Tags->exists($conditions);
342 | $this->assertTrue($actual, 'fail insert.');
343 | }
344 | }
345 |
346 | /**
347 | * bulkUpsert() test by update
348 | *
349 | * @return void
350 | */
351 | public function testBulkUpsertByUpdate()
352 | {
353 | $this->Tags->removeBehavior('Upsert');
354 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
355 | 'updateColumns' => ['description', 'modified'],
356 | ]);
357 |
358 | $records = $this->getBaseUpdateRecords();
359 | $now = DateTime::now();
360 | foreach ($records as $key => $val) {
361 | $records[$key]['created'] = $now;
362 | $records[$key]['modified'] = $now;
363 | }
364 |
365 | $entities = $this->Tags->newEntities($records);
366 | $this->Tags->bulkUpsert($entities);
367 |
368 | $currentCreated = '2017-09-01 00:00:00';
369 | foreach ($records as $conditions) {
370 | $conditions['created'] = $currentCreated;
371 | $actual = $this->Tags->exists($conditions);
372 | $this->assertTrue($actual, 'fail update.');
373 | }
374 | }
375 |
376 | /**
377 | * bulkUpsert() test by update add timestamp behavior
378 | *
379 | * @return void
380 | */
381 | public function testBulkUpsertByUpdateAddTimestamp()
382 | {
383 | $this->Tags->removeBehavior('Upsert');
384 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
385 | 'updateColumns' => ['description', 'modified'],
386 | ]);
387 | $this->Tags->addBehavior('Timestamp');
388 |
389 | $records = $this->getBaseUpdateRecords();
390 | $now = DateTime::now();
391 | $currentCreated = '2017-09-01 00:00:00';
392 | $expectedRecords = $records;
393 | foreach ($expectedRecords as $key => $val) {
394 | $expectedRecords[$key]['created'] = $currentCreated;
395 | $expectedRecords[$key]['modified'] = $now;
396 | }
397 |
398 | $entities = $this->Tags->newEntities($records);
399 | $this->Tags->bulkUpsert($entities);
400 |
401 | foreach ($expectedRecords as $conditions) {
402 | $actual = $this->Tags->exists($conditions);
403 | $this->assertTrue($actual, 'fail update.');
404 | }
405 | }
406 |
407 | /**
408 | * bulkUpsert() test beforeSave not dispatched
409 | *
410 | * @return void
411 | */
412 | public function testBulkUpsertNoBeforeSave()
413 | {
414 | $this->Tags->removeBehavior('Upsert');
415 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
416 | 'updateColumns' => ['description', 'modified'],
417 | 'event' => ['beforeSave' => false],
418 | ]);
419 | $this->Tags->addBehavior('Timestamp');
420 |
421 | $records = $this->getBaseInsertRecords();
422 | $expectedRecords = $records;
423 | foreach ($expectedRecords as $key => $val) {
424 | $expectedRecords[$key]['created IS'] = null;
425 | $expectedRecords[$key]['modified IS'] = null;
426 | }
427 |
428 | $entities = $this->Tags->newEntities($records);
429 | $this->Tags->bulkUpsert($entities);
430 |
431 | foreach ($expectedRecords as $conditions) {
432 | $actual = $this->Tags->exists($conditions);
433 | $this->assertTrue($actual, 'fail insert.');
434 | }
435 | }
436 |
437 | /**
438 | * bulkUpsert() test when invalid update columns
439 | *
440 | * @return void
441 | */
442 | public function testBulkUpsertInvalidUpdateColumnsConfig()
443 | {
444 | $this->expectExceptionMessage('config updateColumns is invalid.');
445 | $this->expectException(LogicException::class);
446 |
447 | $this->Tags->removeBehavior('Upsert');
448 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert');
449 |
450 | $records = $this->getBaseInsertRecords();
451 | $now = DateTime::now();
452 | foreach ($records as $key => $val) {
453 | $records[$key]['created'] = $now;
454 | $records[$key]['modified'] = $now;
455 | }
456 |
457 | $entities = $this->Tags->newEntities($records);
458 | $this->Tags->bulkUpsert($entities);
459 | }
460 |
461 | /**
462 | * bulkUpsert() test by no data
463 | *
464 | * @return void
465 | */
466 | public function testBulkUpsertNoSaveData()
467 | {
468 | $this->expectExceptionMessage('entities has no save data.');
469 | $this->expectException(LogicException::class);
470 |
471 | $this->Tags->removeBehavior('Upsert');
472 | $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [
473 | 'updateColumns' => ['description', 'modified'],
474 | ]);
475 |
476 | $this->Tags->bulkUpsert([]);
477 | }
478 |
479 | /**
480 | * get base insert records
481 | *
482 | * @return array
483 | */
484 | private function getBaseInsertRecords(): array
485 | {
486 | return [
487 | [
488 | 'name' => 'tag4',
489 | 'description' => 'tag4 description',
490 | ],
491 | [
492 | 'name' => 'tag5',
493 | 'description' => 'tag5 description',
494 | ],
495 | [
496 | 'name' => 'tag6',
497 | 'description' => 'tag6 description',
498 | ],
499 | ];
500 | }
501 |
502 | /**
503 | * get base update records
504 | *
505 | * @return array
506 | */
507 | private function getBaseUpdateRecords(): array
508 | {
509 | return [
510 | [
511 | 'name' => 'tag1',
512 | 'description' => 'brand new tag1 description',
513 | ],
514 | [
515 | 'name' => 'tag2',
516 | 'description' => 'brand new tag2 description',
517 | ],
518 | [
519 | 'name' => 'tag3',
520 | 'description' => 'brand new tag3 description',
521 | ],
522 | ];
523 | }
524 | }
525 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | env('DB_URL'),
41 | ];
42 | ConnectionManager::drop('test');
43 | ConnectionManager::setConfig('test', $dbConfig);
44 |
45 | // Create test database schema
46 | if (env('FIXTURE_SCHEMA_METADATA')) {
47 | $loader = new SchemaLoader();
48 | $loader->loadInternalFile(env('FIXTURE_SCHEMA_METADATA'));
49 | }
50 |
--------------------------------------------------------------------------------
/tests/schema.php:
--------------------------------------------------------------------------------
1 | 'articles',
7 | 'columns' => [
8 | 'id' => ['type' => 'integer'],
9 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false],
10 | 'body' => 'text',
11 | 'published' => ['type' => 'integer', 'default' => '0', 'null' => false],
12 | 'created' => 'datetime',
13 | 'modified' => 'datetime',
14 | ],
15 | 'constraints' => [
16 | 'primary' => ['type' => 'primary', 'columns' => ['id']],
17 | ],
18 | ],
19 | [
20 | 'table' => 'tags',
21 | 'columns' => [
22 | 'id' => ['type' => 'integer'],
23 | 'name' => ['type' => 'string', 'length' => 255, 'null' => false],
24 | 'description' => ['type' => 'string', 'length' => 255, 'null' => false],
25 | 'created' => 'datetime',
26 | 'modified' => 'datetime',
27 | ],
28 | 'constraints' => [
29 | 'primary' => ['type' => 'primary', 'columns' => ['id']],
30 | 'unique' => ['type' => 'unique', 'columns' => ['name']],
31 | ],
32 | ],
33 | ];
34 |
--------------------------------------------------------------------------------