PostgresSchemaTest.php 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Database\Schema;
  16. use Cake\Core\Configure;
  17. use Cake\Database\Schema\Collection as SchemaCollection;
  18. use Cake\Database\Schema\MysqlSchema;
  19. use Cake\Database\Schema\PostgresSchema;
  20. use Cake\Database\Schema\Table;
  21. use Cake\Datasource\ConnectionManager;
  22. use Cake\TestSuite\TestCase;
  23. /**
  24. * Postgres schema test case.
  25. */
  26. class PostgresSchemaTest extends TestCase
  27. {
  28. /**
  29. * Helper method for skipping tests that need a real connection.
  30. *
  31. * @return void
  32. */
  33. protected function _needsConnection()
  34. {
  35. $config = ConnectionManager::config('test');
  36. $this->skipIf(strpos($config['driver'], 'Postgres') === false, 'Not using Postgres for test config');
  37. }
  38. /**
  39. * Helper method for testing methods.
  40. *
  41. * @param \Cake\Datasource\ConnectionInterface $connection
  42. * @return void
  43. */
  44. protected function _createTables($connection)
  45. {
  46. $this->_needsConnection();
  47. $connection->execute('DROP TABLE IF EXISTS schema_articles');
  48. $connection->execute('DROP TABLE IF EXISTS schema_authors');
  49. $table = <<<SQL
  50. CREATE TABLE schema_authors (
  51. id SERIAL,
  52. name VARCHAR(50) DEFAULT 'bob',
  53. bio DATE,
  54. position INT DEFAULT 1,
  55. created TIMESTAMP,
  56. PRIMARY KEY (id),
  57. CONSTRAINT "unique_position" UNIQUE ("position")
  58. )
  59. SQL;
  60. $connection->execute($table);
  61. $table = <<<SQL
  62. CREATE TABLE schema_articles (
  63. id BIGINT PRIMARY KEY,
  64. title VARCHAR(20),
  65. body TEXT,
  66. author_id INTEGER NOT NULL,
  67. published BOOLEAN DEFAULT false,
  68. views SMALLINT DEFAULT 0,
  69. readingtime TIME,
  70. data JSONB,
  71. created TIMESTAMP,
  72. CONSTRAINT "content_idx" UNIQUE ("title", "body"),
  73. CONSTRAINT "author_idx" FOREIGN KEY ("author_id") REFERENCES "schema_authors" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
  74. )
  75. SQL;
  76. $connection->execute($table);
  77. $connection->execute('COMMENT ON COLUMN "schema_articles"."title" IS \'a title\'');
  78. $connection->execute('CREATE INDEX "author_idx" ON "schema_articles" ("author_id")');
  79. }
  80. /**
  81. * Data provider for convert column testing
  82. *
  83. * @return array
  84. */
  85. public static function convertColumnProvider()
  86. {
  87. return [
  88. // Timestamp
  89. [
  90. 'TIMESTAMP',
  91. ['type' => 'timestamp', 'length' => null]
  92. ],
  93. [
  94. 'TIMESTAMP WITHOUT TIME ZONE',
  95. ['type' => 'timestamp', 'length' => null]
  96. ],
  97. // Date & time
  98. [
  99. 'DATE',
  100. ['type' => 'date', 'length' => null]
  101. ],
  102. [
  103. 'TIME',
  104. ['type' => 'time', 'length' => null]
  105. ],
  106. [
  107. 'TIME WITHOUT TIME ZONE',
  108. ['type' => 'time', 'length' => null]
  109. ],
  110. // Integer
  111. [
  112. 'SMALLINT',
  113. ['type' => 'integer', 'length' => 5]
  114. ],
  115. [
  116. 'INTEGER',
  117. ['type' => 'integer', 'length' => 10]
  118. ],
  119. [
  120. 'SERIAL',
  121. ['type' => 'integer', 'length' => 10]
  122. ],
  123. [
  124. 'BIGINT',
  125. ['type' => 'biginteger', 'length' => 20]
  126. ],
  127. [
  128. 'BIGSERIAL',
  129. ['type' => 'biginteger', 'length' => 20]
  130. ],
  131. // Decimal
  132. [
  133. 'NUMERIC',
  134. ['type' => 'decimal', 'length' => null]
  135. ],
  136. [
  137. 'DECIMAL(10,2)',
  138. ['type' => 'decimal', 'length' => null]
  139. ],
  140. // String
  141. [
  142. 'VARCHAR',
  143. ['type' => 'string', 'length' => null]
  144. ],
  145. [
  146. 'VARCHAR(10)',
  147. ['type' => 'string', 'length' => 10]
  148. ],
  149. [
  150. 'CHARACTER VARYING',
  151. ['type' => 'string', 'length' => null]
  152. ],
  153. [
  154. 'CHARACTER VARYING(10)',
  155. ['type' => 'string', 'length' => 10]
  156. ],
  157. [
  158. 'CHAR(10)',
  159. ['type' => 'string', 'fixed' => true, 'length' => 10]
  160. ],
  161. [
  162. 'CHARACTER(10)',
  163. ['type' => 'string', 'fixed' => true, 'length' => 10]
  164. ],
  165. [
  166. 'MONEY',
  167. ['type' => 'string', 'length' => null]
  168. ],
  169. // UUID
  170. [
  171. 'UUID',
  172. ['type' => 'uuid', 'length' => null]
  173. ],
  174. [
  175. 'INET',
  176. ['type' => 'string', 'length' => 39]
  177. ],
  178. // Text
  179. [
  180. 'TEXT',
  181. ['type' => 'text', 'length' => null]
  182. ],
  183. // Blob
  184. [
  185. 'BYTEA',
  186. ['type' => 'binary', 'length' => null]
  187. ],
  188. // Float
  189. [
  190. 'REAL',
  191. ['type' => 'float', 'length' => null]
  192. ],
  193. [
  194. 'DOUBLE PRECISION',
  195. ['type' => 'float', 'length' => null]
  196. ],
  197. [
  198. 'JSON',
  199. ['type' => 'json', 'length' => null]
  200. ],
  201. [
  202. 'JSONB',
  203. ['type' => 'json', 'length' => null]
  204. ],
  205. ];
  206. }
  207. /**
  208. * Test parsing Postgres column types from field description.
  209. *
  210. * @dataProvider convertColumnProvider
  211. * @return void
  212. */
  213. public function testConvertColumn($type, $expected)
  214. {
  215. $field = [
  216. 'name' => 'field',
  217. 'type' => $type,
  218. 'null' => 'YES',
  219. 'default' => 'Default value',
  220. 'comment' => 'Comment section',
  221. 'char_length' => null,
  222. ];
  223. $expected += [
  224. 'null' => true,
  225. 'default' => 'Default value',
  226. 'comment' => 'Comment section',
  227. ];
  228. $driver = $this->getMock('Cake\Database\Driver\Postgres');
  229. $dialect = new PostgresSchema($driver);
  230. $table = $this->getMock('Cake\Database\Schema\Table', [], ['table']);
  231. $table->expects($this->at(0))->method('addColumn')->with('field', $expected);
  232. $dialect->convertColumnDescription($table, $field);
  233. }
  234. /**
  235. * Test listing tables with Postgres
  236. *
  237. * @return void
  238. */
  239. public function testListTables()
  240. {
  241. $connection = ConnectionManager::get('test');
  242. $this->_createTables($connection);
  243. $schema = new SchemaCollection($connection);
  244. $result = $schema->listTables();
  245. $this->assertInternalType('array', $result);
  246. $this->assertContains('schema_articles', $result);
  247. $this->assertContains('schema_authors', $result);
  248. }
  249. /**
  250. * Test that describe accepts tablenames containing `schema.table`.
  251. *
  252. * @return void
  253. */
  254. public function testDescribeWithSchemaName()
  255. {
  256. $connection = ConnectionManager::get('test');
  257. $this->_createTables($connection);
  258. $schema = new SchemaCollection($connection);
  259. $result = $schema->describe('public.schema_articles');
  260. $this->assertEquals(['id'], $result->primaryKey());
  261. $this->assertEquals('schema_articles', $result->name());
  262. }
  263. /**
  264. * Test describing a table with Postgres
  265. *
  266. * @return void
  267. */
  268. public function testDescribeTable()
  269. {
  270. $connection = ConnectionManager::get('test');
  271. $this->_createTables($connection);
  272. $schema = new SchemaCollection($connection);
  273. $result = $schema->describe('schema_articles');
  274. $expected = [
  275. 'id' => [
  276. 'type' => 'biginteger',
  277. 'null' => false,
  278. 'default' => null,
  279. 'length' => 20,
  280. 'precision' => null,
  281. 'unsigned' => null,
  282. 'comment' => null,
  283. 'autoIncrement' => false,
  284. ],
  285. 'title' => [
  286. 'type' => 'string',
  287. 'null' => true,
  288. 'default' => null,
  289. 'length' => 20,
  290. 'precision' => null,
  291. 'comment' => 'a title',
  292. 'fixed' => null,
  293. ],
  294. 'body' => [
  295. 'type' => 'text',
  296. 'null' => true,
  297. 'default' => null,
  298. 'length' => null,
  299. 'precision' => null,
  300. 'comment' => null,
  301. ],
  302. 'author_id' => [
  303. 'type' => 'integer',
  304. 'null' => false,
  305. 'default' => null,
  306. 'length' => 10,
  307. 'precision' => null,
  308. 'unsigned' => null,
  309. 'comment' => null,
  310. 'autoIncrement' => null,
  311. ],
  312. 'published' => [
  313. 'type' => 'boolean',
  314. 'null' => true,
  315. 'default' => 0,
  316. 'length' => null,
  317. 'precision' => null,
  318. 'comment' => null,
  319. ],
  320. 'views' => [
  321. 'type' => 'integer',
  322. 'null' => true,
  323. 'default' => 0,
  324. 'length' => 5,
  325. 'precision' => null,
  326. 'unsigned' => null,
  327. 'comment' => null,
  328. 'autoIncrement' => null,
  329. ],
  330. 'readingtime' => [
  331. 'type' => 'time',
  332. 'null' => true,
  333. 'default' => null,
  334. 'length' => null,
  335. 'precision' => null,
  336. 'comment' => null,
  337. ],
  338. 'data' => [
  339. 'type' => 'json',
  340. 'null' => true,
  341. 'default' => null,
  342. 'length' => null,
  343. 'precision' => null,
  344. 'comment' => null,
  345. ],
  346. 'created' => [
  347. 'type' => 'timestamp',
  348. 'null' => true,
  349. 'default' => null,
  350. 'length' => null,
  351. 'precision' => null,
  352. 'comment' => null,
  353. ],
  354. ];
  355. $this->assertEquals(['id'], $result->primaryKey());
  356. foreach ($expected as $field => $definition) {
  357. $this->assertEquals($definition, $result->column($field));
  358. }
  359. }
  360. /**
  361. * Test describing a table with postgres and composite keys
  362. *
  363. * @return void
  364. */
  365. public function testDescribeTableCompositeKey()
  366. {
  367. $this->_needsConnection();
  368. $connection = ConnectionManager::get('test');
  369. $sql = <<<SQL
  370. CREATE TABLE schema_composite (
  371. "id" SERIAL,
  372. "site_id" INTEGER NOT NULL,
  373. "name" VARCHAR(255),
  374. PRIMARY KEY("id", "site_id")
  375. );
  376. SQL;
  377. $connection->execute($sql);
  378. $schema = new SchemaCollection($connection);
  379. $result = $schema->describe('schema_composite');
  380. $connection->execute('DROP TABLE schema_composite');
  381. $this->assertEquals(['id', 'site_id'], $result->primaryKey());
  382. $this->assertTrue($result->column('id')['autoIncrement'], 'id should be autoincrement');
  383. $this->assertNull($result->column('site_id')['autoIncrement'], 'site_id should not be autoincrement');
  384. }
  385. /**
  386. * Test describing a table containing defaults with Postgres
  387. *
  388. * @return void
  389. */
  390. public function testDescribeTableWithDefaults()
  391. {
  392. $connection = ConnectionManager::get('test');
  393. $this->_createTables($connection);
  394. $schema = new SchemaCollection($connection);
  395. $result = $schema->describe('schema_authors');
  396. $expected = [
  397. 'id' => [
  398. 'type' => 'integer',
  399. 'null' => false,
  400. 'default' => null,
  401. 'length' => 10,
  402. 'precision' => null,
  403. 'unsigned' => null,
  404. 'comment' => null,
  405. 'autoIncrement' => true,
  406. ],
  407. 'name' => [
  408. 'type' => 'string',
  409. 'null' => true,
  410. 'default' => 'bob',
  411. 'length' => 50,
  412. 'precision' => null,
  413. 'comment' => null,
  414. 'fixed' => null,
  415. ],
  416. 'bio' => [
  417. 'type' => 'date',
  418. 'null' => true,
  419. 'default' => null,
  420. 'length' => null,
  421. 'precision' => null,
  422. 'comment' => null,
  423. ],
  424. 'position' => [
  425. 'type' => 'integer',
  426. 'null' => true,
  427. 'default' => '1',
  428. 'length' => 10,
  429. 'precision' => null,
  430. 'comment' => null,
  431. 'unsigned' => null,
  432. 'autoIncrement' => null,
  433. ],
  434. 'created' => [
  435. 'type' => 'timestamp',
  436. 'null' => true,
  437. 'default' => null,
  438. 'length' => null,
  439. 'precision' => null,
  440. 'comment' => null,
  441. ],
  442. ];
  443. $this->assertEquals(['id'], $result->primaryKey());
  444. foreach ($expected as $field => $definition) {
  445. $this->assertEquals($definition, $result->column($field), "Mismatch in $field column");
  446. }
  447. }
  448. /**
  449. * Test describing a table with containing keywords
  450. *
  451. * @return void
  452. */
  453. public function testDescribeTableConstraintsWithKeywords()
  454. {
  455. $connection = ConnectionManager::get('test');
  456. $this->_createTables($connection);
  457. $schema = new SchemaCollection($connection);
  458. $result = $schema->describe('schema_authors');
  459. $this->assertInstanceOf('Cake\Database\Schema\Table', $result);
  460. $expected = [
  461. 'primary' => [
  462. 'type' => 'primary',
  463. 'columns' => ['id'],
  464. 'length' => []
  465. ],
  466. 'unique_position' => [
  467. 'type' => 'unique',
  468. 'columns' => ['position'],
  469. 'length' => []
  470. ]
  471. ];
  472. $this->assertCount(2, $result->constraints());
  473. $this->assertEquals($expected['primary'], $result->constraint('primary'));
  474. $this->assertEquals($expected['unique_position'], $result->constraint('unique_position'));
  475. }
  476. /**
  477. * Test describing a table with indexes
  478. *
  479. * @return void
  480. */
  481. public function testDescribeTableIndexes()
  482. {
  483. $connection = ConnectionManager::get('test');
  484. $this->_createTables($connection);
  485. $schema = new SchemaCollection($connection);
  486. $result = $schema->describe('schema_articles');
  487. $this->assertInstanceOf('Cake\Database\Schema\Table', $result);
  488. $expected = [
  489. 'primary' => [
  490. 'type' => 'primary',
  491. 'columns' => ['id'],
  492. 'length' => []
  493. ],
  494. 'content_idx' => [
  495. 'type' => 'unique',
  496. 'columns' => ['title', 'body'],
  497. 'length' => []
  498. ]
  499. ];
  500. $this->assertCount(3, $result->constraints());
  501. $expected = [
  502. 'primary' => [
  503. 'type' => 'primary',
  504. 'columns' => ['id'],
  505. 'length' => []
  506. ],
  507. 'content_idx' => [
  508. 'type' => 'unique',
  509. 'columns' => ['title', 'body'],
  510. 'length' => []
  511. ],
  512. 'author_idx' => [
  513. 'type' => 'foreign',
  514. 'columns' => ['author_id'],
  515. 'references' => ['schema_authors', 'id'],
  516. 'length' => [],
  517. 'update' => 'cascade',
  518. 'delete' => 'restrict',
  519. ]
  520. ];
  521. $this->assertEquals($expected['primary'], $result->constraint('primary'));
  522. $this->assertEquals($expected['content_idx'], $result->constraint('content_idx'));
  523. $this->assertEquals($expected['author_idx'], $result->constraint('author_idx'));
  524. $this->assertCount(1, $result->indexes());
  525. $expected = [
  526. 'type' => 'index',
  527. 'columns' => ['author_id'],
  528. 'length' => []
  529. ];
  530. $this->assertEquals($expected, $result->index('author_idx'));
  531. }
  532. /**
  533. * Test describing a table with indexes with nulls first
  534. *
  535. * @return void
  536. */
  537. public function testDescribeTableIndexesNullsFirst()
  538. {
  539. $this->_needsConnection();
  540. $connection = ConnectionManager::get('test');
  541. $connection->execute('DROP TABLE IF EXISTS schema_index');
  542. $table = <<<SQL
  543. CREATE TABLE schema_index (
  544. id serial NOT NULL,
  545. user_id integer NOT NULL,
  546. group_id integer NOT NULL,
  547. grade double precision
  548. )
  549. WITH (
  550. OIDS=FALSE
  551. )
  552. SQL;
  553. $connection->execute($table);
  554. $index = <<<SQL
  555. CREATE INDEX schema_index_nulls
  556. ON schema_index
  557. USING btree
  558. (group_id, grade DESC NULLS FIRST);
  559. SQL;
  560. $connection->execute($index);
  561. $schema = new SchemaCollection($connection);
  562. $result = $schema->describe('schema_index');
  563. $this->assertCount(1, $result->indexes());
  564. $expected = [
  565. 'type' => 'index',
  566. 'columns' => ['group_id', 'grade'],
  567. 'length' => []
  568. ];
  569. $this->assertEquals($expected, $result->index('schema_index_nulls'));
  570. $connection->execute('DROP TABLE schema_index');
  571. }
  572. /**
  573. * Column provider for creating column sql
  574. *
  575. * @return array
  576. */
  577. public static function columnSqlProvider()
  578. {
  579. return [
  580. // strings
  581. [
  582. 'title',
  583. ['type' => 'string', 'length' => 25, 'null' => false],
  584. '"title" VARCHAR(25) NOT NULL'
  585. ],
  586. [
  587. 'title',
  588. ['type' => 'string', 'length' => 25, 'null' => true, 'default' => 'ignored'],
  589. '"title" VARCHAR(25) DEFAULT NULL'
  590. ],
  591. [
  592. 'id',
  593. ['type' => 'string', 'length' => 32, 'fixed' => true, 'null' => false],
  594. '"id" CHAR(32) NOT NULL'
  595. ],
  596. [
  597. 'id',
  598. ['type' => 'uuid', 'length' => 36, 'null' => false],
  599. '"id" UUID NOT NULL'
  600. ],
  601. [
  602. 'role',
  603. ['type' => 'string', 'length' => 10, 'null' => false, 'default' => 'admin'],
  604. '"role" VARCHAR(10) NOT NULL DEFAULT \'admin\''
  605. ],
  606. [
  607. 'title',
  608. ['type' => 'string'],
  609. '"title" VARCHAR'
  610. ],
  611. // Text
  612. [
  613. 'body',
  614. ['type' => 'text', 'null' => false],
  615. '"body" TEXT NOT NULL'
  616. ],
  617. [
  618. 'body',
  619. ['type' => 'text', 'length' => Table::LENGTH_TINY, 'null' => false],
  620. sprintf('"body" VARCHAR(%s) NOT NULL', Table::LENGTH_TINY)
  621. ],
  622. [
  623. 'body',
  624. ['type' => 'text', 'length' => Table::LENGTH_MEDIUM, 'null' => false],
  625. '"body" TEXT NOT NULL'
  626. ],
  627. [
  628. 'body',
  629. ['type' => 'text', 'length' => Table::LENGTH_LONG, 'null' => false],
  630. '"body" TEXT NOT NULL'
  631. ],
  632. // Integers
  633. [
  634. 'post_id',
  635. ['type' => 'integer', 'length' => 11],
  636. '"post_id" INTEGER'
  637. ],
  638. [
  639. 'post_id',
  640. ['type' => 'biginteger', 'length' => 20],
  641. '"post_id" BIGINT'
  642. ],
  643. [
  644. 'post_id',
  645. ['type' => 'integer', 'autoIncrement' => true, 'length' => 11],
  646. '"post_id" SERIAL'
  647. ],
  648. [
  649. 'post_id',
  650. ['type' => 'biginteger', 'autoIncrement' => true, 'length' => 20],
  651. '"post_id" BIGSERIAL'
  652. ],
  653. // Decimal
  654. [
  655. 'value',
  656. ['type' => 'decimal'],
  657. '"value" DECIMAL'
  658. ],
  659. [
  660. 'value',
  661. ['type' => 'decimal', 'length' => 11],
  662. '"value" DECIMAL(11,0)'
  663. ],
  664. [
  665. 'value',
  666. ['type' => 'decimal', 'length' => 12, 'precision' => 5],
  667. '"value" DECIMAL(12,5)'
  668. ],
  669. // Float
  670. [
  671. 'value',
  672. ['type' => 'float'],
  673. '"value" FLOAT'
  674. ],
  675. [
  676. 'value',
  677. ['type' => 'float', 'length' => 11, 'precision' => 3],
  678. '"value" FLOAT(3)'
  679. ],
  680. // Binary
  681. [
  682. 'img',
  683. ['type' => 'binary'],
  684. '"img" BYTEA'
  685. ],
  686. // Boolean
  687. [
  688. 'checked',
  689. ['type' => 'boolean', 'default' => false],
  690. '"checked" BOOLEAN DEFAULT FALSE'
  691. ],
  692. [
  693. 'checked',
  694. ['type' => 'boolean', 'default' => true, 'null' => false],
  695. '"checked" BOOLEAN NOT NULL DEFAULT TRUE'
  696. ],
  697. // Boolean
  698. [
  699. 'checked',
  700. ['type' => 'boolean', 'default' => 0],
  701. '"checked" BOOLEAN DEFAULT FALSE'
  702. ],
  703. [
  704. 'checked',
  705. ['type' => 'boolean', 'default' => 1, 'null' => false],
  706. '"checked" BOOLEAN NOT NULL DEFAULT TRUE'
  707. ],
  708. // datetimes
  709. [
  710. 'created',
  711. ['type' => 'datetime'],
  712. '"created" TIMESTAMP'
  713. ],
  714. // Date & Time
  715. [
  716. 'start_date',
  717. ['type' => 'date'],
  718. '"start_date" DATE'
  719. ],
  720. [
  721. 'start_time',
  722. ['type' => 'time'],
  723. '"start_time" TIME'
  724. ],
  725. // timestamps
  726. [
  727. 'created',
  728. ['type' => 'timestamp', 'null' => true],
  729. '"created" TIMESTAMP DEFAULT NULL'
  730. ],
  731. ];
  732. }
  733. /**
  734. * Test generating column definitions
  735. *
  736. * @dataProvider columnSqlProvider
  737. * @return void
  738. */
  739. public function testColumnSql($name, $data, $expected)
  740. {
  741. $driver = $this->_getMockedDriver();
  742. $schema = new PostgresSchema($driver);
  743. $table = (new Table('schema_articles'))->addColumn($name, $data);
  744. $this->assertEquals($expected, $schema->columnSql($table, $name));
  745. }
  746. /**
  747. * Test generating a column that is a primary key.
  748. *
  749. * @return void
  750. */
  751. public function testColumnSqlPrimaryKey()
  752. {
  753. $driver = $this->_getMockedDriver();
  754. $schema = new PostgresSchema($driver);
  755. $table = new Table('schema_articles');
  756. $table->addColumn('id', [
  757. 'type' => 'integer',
  758. 'null' => false
  759. ])
  760. ->addConstraint('primary', [
  761. 'type' => 'primary',
  762. 'columns' => ['id']
  763. ]);
  764. $result = $schema->columnSql($table, 'id');
  765. $this->assertEquals($result, '"id" SERIAL');
  766. }
  767. /**
  768. * Provide data for testing constraintSql
  769. *
  770. * @return array
  771. */
  772. public static function constraintSqlProvider()
  773. {
  774. return [
  775. [
  776. 'primary',
  777. ['type' => 'primary', 'columns' => ['title']],
  778. 'PRIMARY KEY ("title")'
  779. ],
  780. [
  781. 'unique_idx',
  782. ['type' => 'unique', 'columns' => ['title', 'author_id']],
  783. 'CONSTRAINT "unique_idx" UNIQUE ("title", "author_id")'
  784. ],
  785. [
  786. 'author_id_idx',
  787. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id']],
  788. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  789. 'REFERENCES "authors" ("id") ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE'
  790. ],
  791. [
  792. 'author_id_idx',
  793. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'cascade'],
  794. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  795. 'REFERENCES "authors" ("id") ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE'
  796. ],
  797. [
  798. 'author_id_idx',
  799. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'restrict'],
  800. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  801. 'REFERENCES "authors" ("id") ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE'
  802. ],
  803. [
  804. 'author_id_idx',
  805. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'setNull'],
  806. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  807. 'REFERENCES "authors" ("id") ON UPDATE SET NULL ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE'
  808. ],
  809. [
  810. 'author_id_idx',
  811. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'noAction'],
  812. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  813. 'REFERENCES "authors" ("id") ON UPDATE NO ACTION ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE'
  814. ],
  815. ];
  816. }
  817. /**
  818. * Test the constraintSql method.
  819. *
  820. * @dataProvider constraintSqlProvider
  821. */
  822. public function testConstraintSql($name, $data, $expected)
  823. {
  824. $driver = $this->_getMockedDriver();
  825. $schema = new PostgresSchema($driver);
  826. $table = (new Table('schema_articles'))->addColumn('title', [
  827. 'type' => 'string',
  828. 'length' => 255
  829. ])->addColumn('author_id', [
  830. 'type' => 'integer',
  831. ])->addConstraint($name, $data);
  832. $this->assertTextEquals($expected, $schema->constraintSql($table, $name));
  833. }
  834. /**
  835. * Test the addConstraintSql method.
  836. *
  837. * @return void
  838. */
  839. public function testAddConstraintSql()
  840. {
  841. $driver = $this->_getMockedDriver();
  842. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  843. $connection->expects($this->any())->method('driver')
  844. ->will($this->returnValue($driver));
  845. $table = (new Table('posts'))
  846. ->addColumn('author_id', [
  847. 'type' => 'integer',
  848. 'null' => false
  849. ])
  850. ->addColumn('category_id', [
  851. 'type' => 'integer',
  852. 'null' => false
  853. ])
  854. ->addColumn('category_name', [
  855. 'type' => 'integer',
  856. 'null' => false
  857. ])
  858. ->addConstraint('author_fk', [
  859. 'type' => 'foreign',
  860. 'columns' => ['author_id'],
  861. 'references' => ['authors', 'id'],
  862. 'update' => 'cascade',
  863. 'delete' => 'cascade'
  864. ])
  865. ->addConstraint('category_fk', [
  866. 'type' => 'foreign',
  867. 'columns' => ['category_id', 'category_name'],
  868. 'references' => ['categories', ['id', 'name']],
  869. 'update' => 'cascade',
  870. 'delete' => 'cascade'
  871. ]);
  872. $expected = [
  873. 'ALTER TABLE "posts" ADD CONSTRAINT "author_fk" FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE;',
  874. 'ALTER TABLE "posts" ADD CONSTRAINT "category_fk" FOREIGN KEY ("category_id", "category_name") REFERENCES "categories" ("id", "name") ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE;'
  875. ];
  876. $result = $table->addConstraintSql($connection);
  877. $this->assertCount(2, $result);
  878. $this->assertEquals($expected, $result);
  879. }
  880. /**
  881. * Test the dropConstraintSql method.
  882. *
  883. * @return void
  884. */
  885. public function testDropConstraintSql()
  886. {
  887. $driver = $this->_getMockedDriver();
  888. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  889. $connection->expects($this->any())->method('driver')
  890. ->will($this->returnValue($driver));
  891. $table = (new Table('posts'))
  892. ->addColumn('author_id', [
  893. 'type' => 'integer',
  894. 'null' => false
  895. ])
  896. ->addColumn('category_id', [
  897. 'type' => 'integer',
  898. 'null' => false
  899. ])
  900. ->addColumn('category_name', [
  901. 'type' => 'integer',
  902. 'null' => false
  903. ])
  904. ->addConstraint('author_fk', [
  905. 'type' => 'foreign',
  906. 'columns' => ['author_id'],
  907. 'references' => ['authors', 'id'],
  908. 'update' => 'cascade',
  909. 'delete' => 'cascade'
  910. ])
  911. ->addConstraint('category_fk', [
  912. 'type' => 'foreign',
  913. 'columns' => ['category_id', 'category_name'],
  914. 'references' => ['categories', ['id', 'name']],
  915. 'update' => 'cascade',
  916. 'delete' => 'cascade'
  917. ]);
  918. $expected = [
  919. 'ALTER TABLE "posts" DROP CONSTRAINT "author_fk";',
  920. 'ALTER TABLE "posts" DROP CONSTRAINT "category_fk";'
  921. ];
  922. $result = $table->dropConstraintSql($connection);
  923. $this->assertCount(2, $result);
  924. $this->assertEquals($expected, $result);
  925. }
  926. /**
  927. * Integration test for converting a Schema\Table into MySQL table creates.
  928. *
  929. * @return void
  930. */
  931. public function testCreateSql()
  932. {
  933. $driver = $this->_getMockedDriver();
  934. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  935. $connection->expects($this->any())->method('driver')
  936. ->will($this->returnValue($driver));
  937. $table = (new Table('schema_articles'))->addColumn('id', [
  938. 'type' => 'integer',
  939. 'null' => false
  940. ])
  941. ->addColumn('title', [
  942. 'type' => 'string',
  943. 'null' => false,
  944. 'comment' => 'This is the title',
  945. ])
  946. ->addColumn('body', ['type' => 'text'])
  947. ->addColumn('data', ['type' => 'json'])
  948. ->addColumn('created', 'datetime')
  949. ->addConstraint('primary', [
  950. 'type' => 'primary',
  951. 'columns' => ['id'],
  952. ])
  953. ->addIndex('title_idx', [
  954. 'type' => 'index',
  955. 'columns' => ['title'],
  956. ]);
  957. $expected = <<<SQL
  958. CREATE TABLE "schema_articles" (
  959. "id" SERIAL,
  960. "title" VARCHAR NOT NULL,
  961. "body" TEXT,
  962. "data" JSONB,
  963. "created" TIMESTAMP,
  964. PRIMARY KEY ("id")
  965. )
  966. SQL;
  967. $result = $table->createSql($connection);
  968. $this->assertCount(3, $result);
  969. $this->assertTextEquals($expected, $result[0]);
  970. $this->assertEquals(
  971. 'CREATE INDEX "title_idx" ON "schema_articles" ("title")',
  972. $result[1]
  973. );
  974. $this->assertEquals(
  975. 'COMMENT ON COLUMN "schema_articles"."title" IS \'This is the title\'',
  976. $result[2]
  977. );
  978. }
  979. /**
  980. * Tests creating temporary tables
  981. *
  982. * @return void
  983. */
  984. public function testCreateTemporary()
  985. {
  986. $driver = $this->_getMockedDriver();
  987. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  988. $connection->expects($this->any())->method('driver')
  989. ->will($this->returnValue($driver));
  990. $table = (new Table('schema_articles'))->addColumn('id', [
  991. 'type' => 'integer',
  992. 'null' => false
  993. ]);
  994. $table->temporary(true);
  995. $sql = $table->createSql($connection);
  996. $this->assertContains('CREATE TEMPORARY TABLE', $sql[0]);
  997. }
  998. /**
  999. * Test primary key generation & auto-increment.
  1000. *
  1001. * @return void
  1002. */
  1003. public function testCreateSqlCompositeIntegerKey()
  1004. {
  1005. $driver = $this->_getMockedDriver();
  1006. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  1007. $connection->expects($this->any())->method('driver')
  1008. ->will($this->returnValue($driver));
  1009. $table = (new Table('articles_tags'))
  1010. ->addColumn('article_id', [
  1011. 'type' => 'integer',
  1012. 'null' => false
  1013. ])
  1014. ->addColumn('tag_id', [
  1015. 'type' => 'integer',
  1016. 'null' => false,
  1017. ])
  1018. ->addConstraint('primary', [
  1019. 'type' => 'primary',
  1020. 'columns' => ['article_id', 'tag_id']
  1021. ]);
  1022. $expected = <<<SQL
  1023. CREATE TABLE "articles_tags" (
  1024. "article_id" INTEGER NOT NULL,
  1025. "tag_id" INTEGER NOT NULL,
  1026. PRIMARY KEY ("article_id", "tag_id")
  1027. )
  1028. SQL;
  1029. $result = $table->createSql($connection);
  1030. $this->assertCount(1, $result);
  1031. $this->assertTextEquals($expected, $result[0]);
  1032. $table = (new Table('composite_key'))
  1033. ->addColumn('id', [
  1034. 'type' => 'integer',
  1035. 'null' => false,
  1036. 'autoIncrement' => true
  1037. ])
  1038. ->addColumn('account_id', [
  1039. 'type' => 'integer',
  1040. 'null' => false,
  1041. ])
  1042. ->addConstraint('primary', [
  1043. 'type' => 'primary',
  1044. 'columns' => ['id', 'account_id']
  1045. ]);
  1046. $expected = <<<SQL
  1047. CREATE TABLE "composite_key" (
  1048. "id" SERIAL,
  1049. "account_id" INTEGER NOT NULL,
  1050. PRIMARY KEY ("id", "account_id")
  1051. )
  1052. SQL;
  1053. $result = $table->createSql($connection);
  1054. $this->assertCount(1, $result);
  1055. $this->assertTextEquals($expected, $result[0]);
  1056. }
  1057. /**
  1058. * test dropSql
  1059. *
  1060. * @return void
  1061. */
  1062. public function testDropSql()
  1063. {
  1064. $driver = $this->_getMockedDriver();
  1065. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  1066. $connection->expects($this->any())->method('driver')
  1067. ->will($this->returnValue($driver));
  1068. $table = new Table('schema_articles');
  1069. $result = $table->dropSql($connection);
  1070. $this->assertCount(1, $result);
  1071. $this->assertEquals('DROP TABLE "schema_articles" CASCADE', $result[0]);
  1072. }
  1073. /**
  1074. * Test truncateSql()
  1075. *
  1076. * @return void
  1077. */
  1078. public function testTruncateSql()
  1079. {
  1080. $driver = $this->_getMockedDriver();
  1081. $connection = $this->getMock('Cake\Database\Connection', [], [], '', false);
  1082. $connection->expects($this->any())->method('driver')
  1083. ->will($this->returnValue($driver));
  1084. $table = new Table('schema_articles');
  1085. $table->addColumn('id', 'integer')
  1086. ->addConstraint('primary', [
  1087. 'type' => 'primary',
  1088. 'columns' => ['id']
  1089. ]);
  1090. $result = $table->truncateSql($connection);
  1091. $this->assertCount(1, $result);
  1092. $this->assertEquals('TRUNCATE "schema_articles" RESTART IDENTITY CASCADE', $result[0]);
  1093. }
  1094. /**
  1095. * Get a schema instance with a mocked driver/pdo instances
  1096. *
  1097. * @return Driver
  1098. */
  1099. protected function _getMockedDriver()
  1100. {
  1101. $driver = new \Cake\Database\Driver\Postgres();
  1102. $mock = $this->getMock('FakePdo', ['quote']);
  1103. $mock->expects($this->any())
  1104. ->method('quote')
  1105. ->will($this->returnCallback(function ($value) {
  1106. return "'$value'";
  1107. }));
  1108. $driver->connection($mock);
  1109. return $driver;
  1110. }
  1111. }