PostgresSchemaTest.php 46 KB

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