├── .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 | [![codecov](https://codecov.io/gh/itosho/easy-query/branch/master/graph/badge.svg)](https://codecov.io/gh/itosho/easy-query) 5 | [![Latest Stable Version](https://poser.pugx.org/itosho/easy-query/v/stable)](https://packagist.org/packages/itosho/easy-query) 6 | [![Total Downloads](https://poser.pugx.org/itosho/easy-query/downloads)](https://packagist.org/packages/itosho/easy-query) 7 | [![License](https://poser.pugx.org/itosho/easy-query/license)](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 | --------------------------------------------------------------------------------