SqliteSchemaTest.php 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  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\Sqlite;
  19. use Cake\Database\Schema\Collection as SchemaCollection;
  20. use Cake\Database\Schema\SqliteSchemaDialect;
  21. use Cake\Database\Schema\TableSchema;
  22. use Cake\Datasource\ConnectionManager;
  23. use Cake\TestSuite\TestCase;
  24. use PDO;
  25. /**
  26. * Test case for Sqlite Schema Dialect.
  27. */
  28. class SqliteSchemaTest 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'], 'Sqlite') === false, 'Not using Sqlite for test config');
  37. }
  38. /**
  39. * Data provider for convert column testing
  40. *
  41. * @return array
  42. */
  43. public static function convertColumnProvider(): array
  44. {
  45. return [
  46. [
  47. 'DATETIME',
  48. ['type' => 'datetime', 'length' => null],
  49. ],
  50. [
  51. 'DATE',
  52. ['type' => 'date', 'length' => null],
  53. ],
  54. [
  55. 'TIME',
  56. ['type' => 'time', 'length' => null],
  57. ],
  58. [
  59. 'BOOLEAN',
  60. ['type' => 'boolean', 'length' => null],
  61. ],
  62. [
  63. 'BIGINT',
  64. ['type' => 'biginteger', 'length' => null, 'unsigned' => false],
  65. ],
  66. [
  67. 'UNSIGNED BIGINT',
  68. ['type' => 'biginteger', 'length' => null, 'unsigned' => true],
  69. ],
  70. [
  71. 'VARCHAR(255)',
  72. ['type' => 'string', 'length' => 255],
  73. ],
  74. [
  75. 'CHAR(25)',
  76. ['type' => 'char', 'length' => 25],
  77. ],
  78. [
  79. 'CHAR(36)',
  80. ['type' => 'uuid', 'length' => null],
  81. ],
  82. [
  83. 'BINARY(16)',
  84. ['type' => 'binaryuuid', 'length' => null],
  85. ],
  86. [
  87. 'BINARY(1)',
  88. ['type' => 'binary', 'length' => 1],
  89. ],
  90. [
  91. 'BLOB',
  92. ['type' => 'binary', 'length' => null],
  93. ],
  94. [
  95. 'INTEGER(11)',
  96. ['type' => 'integer', 'length' => 11, 'unsigned' => false],
  97. ],
  98. [
  99. 'UNSIGNED INTEGER(11)',
  100. ['type' => 'integer', 'length' => 11, 'unsigned' => true],
  101. ],
  102. [
  103. 'TINYINT(3)',
  104. ['type' => 'tinyinteger', 'length' => 3, 'unsigned' => false],
  105. ],
  106. [
  107. 'UNSIGNED TINYINT(3)',
  108. ['type' => 'tinyinteger', 'length' => 3, 'unsigned' => true],
  109. ],
  110. [
  111. 'SMALLINT(5)',
  112. ['type' => 'smallinteger', 'length' => 5, 'unsigned' => false],
  113. ],
  114. [
  115. 'UNSIGNED SMALLINT(5)',
  116. ['type' => 'smallinteger', 'length' => 5, 'unsigned' => true],
  117. ],
  118. [
  119. 'MEDIUMINT(10)',
  120. ['type' => 'integer', 'length' => 10, 'unsigned' => false],
  121. ],
  122. [
  123. 'FLOAT',
  124. ['type' => 'float', 'length' => null, 'precision' => null, 'unsigned' => false],
  125. ],
  126. [
  127. 'DOUBLE',
  128. ['type' => 'float', 'length' => null, 'precision' => null, 'unsigned' => false],
  129. ],
  130. [
  131. 'UNSIGNED DOUBLE',
  132. ['type' => 'float', 'length' => null, 'precision' => null, 'unsigned' => true],
  133. ],
  134. [
  135. 'REAL',
  136. ['type' => 'float', 'length' => null, 'precision' => null, 'unsigned' => false],
  137. ],
  138. [
  139. 'DECIMAL(11,2)',
  140. ['type' => 'decimal', 'length' => 11, 'precision' => 2, 'unsigned' => false],
  141. ],
  142. [
  143. 'UNSIGNED DECIMAL(11,2)',
  144. ['type' => 'decimal', 'length' => 11, 'precision' => 2, 'unsigned' => true],
  145. ],
  146. ];
  147. }
  148. /**
  149. * Test parsing SQLite column types from field description.
  150. *
  151. * @dataProvider convertColumnProvider
  152. */
  153. public function testConvertColumn(string $type, array $expected): void
  154. {
  155. $field = [
  156. 'pk' => false,
  157. 'name' => 'field',
  158. 'type' => $type,
  159. 'notnull' => false,
  160. 'dflt_value' => 'Default value',
  161. ];
  162. $expected += [
  163. 'null' => true,
  164. 'default' => 'Default value',
  165. 'comment' => null,
  166. ];
  167. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlite')->getMock();
  168. $dialect = new SqliteSchemaDialect($driver);
  169. $table = new TableSchema('table');
  170. $dialect->convertColumnDescription($table, $field);
  171. $actual = array_intersect_key($table->getColumn('field'), $expected);
  172. ksort($expected);
  173. ksort($actual);
  174. $this->assertSame($expected, $actual);
  175. }
  176. /**
  177. * Tests converting multiple rows into a primary constraint with multiple
  178. * columns
  179. */
  180. public function testConvertCompositePrimaryKey(): void
  181. {
  182. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlite')->getMock();
  183. $dialect = new SqliteSchemaDialect($driver);
  184. $field1 = [
  185. 'pk' => true,
  186. 'name' => 'field1',
  187. 'type' => 'INTEGER(11)',
  188. 'notnull' => false,
  189. 'dflt_value' => 0,
  190. ];
  191. $field2 = [
  192. 'pk' => true,
  193. 'name' => 'field2',
  194. 'type' => 'INTEGER(11)',
  195. 'notnull' => false,
  196. 'dflt_value' => 1,
  197. ];
  198. $table = new TableSchema('table');
  199. $dialect->convertColumnDescription($table, $field1);
  200. $dialect->convertColumnDescription($table, $field2);
  201. $this->assertEquals(['field1', 'field2'], $table->getPrimaryKey());
  202. }
  203. /**
  204. * Creates tables for testing listTables/describe()
  205. *
  206. * @param \Cake\Database\Connection $connection
  207. */
  208. protected function _createTables($connection): void
  209. {
  210. $this->_needsConnection();
  211. $schema = new SchemaCollection($connection);
  212. $result = $schema->listTables();
  213. if (
  214. in_array('schema_articles', $result) &&
  215. in_array('schema_authors', $result)
  216. ) {
  217. return;
  218. }
  219. $table = <<<SQL
  220. CREATE TABLE schema_authors (
  221. id INTEGER PRIMARY KEY AUTOINCREMENT,
  222. name VARCHAR(50),
  223. bio TEXT,
  224. created DATETIME
  225. )
  226. SQL;
  227. $connection->execute($table);
  228. $table = <<<SQL
  229. CREATE TABLE schema_articles (
  230. id INTEGER PRIMARY KEY AUTOINCREMENT,
  231. title VARCHAR(20) DEFAULT 'Let ''em eat cake',
  232. body TEXT,
  233. author_id INT(11) NOT NULL,
  234. published BOOLEAN DEFAULT 0,
  235. created DATETIME,
  236. field1 VARCHAR(10) DEFAULT NULL,
  237. field2 VARCHAR(10) DEFAULT 'NULL',
  238. CONSTRAINT "title_idx" UNIQUE ("title", "body")
  239. CONSTRAINT "author_idx" FOREIGN KEY ("author_id") REFERENCES "schema_authors" ("id") ON UPDATE CASCADE ON DELETE RESTRICT
  240. );
  241. SQL;
  242. $connection->execute($table);
  243. $connection->execute('CREATE INDEX "created_idx" ON "schema_articles" ("created")');
  244. $sql = <<<SQL
  245. CREATE TABLE schema_composite (
  246. "id" INTEGER NOT NULL,
  247. "site_id" INTEGER NOT NULL,
  248. "name" VARCHAR(255),
  249. PRIMARY KEY("id", "site_id")
  250. );
  251. SQL;
  252. $connection->execute($sql);
  253. $view = <<<SQL
  254. CREATE VIEW view_schema_articles AS
  255. SELECT count(*) as total FROM schema_articles
  256. SQL;
  257. $connection->execute($view);
  258. }
  259. /**
  260. * Test SchemaCollection listing tables with Sqlite
  261. */
  262. public function testListTables(): void
  263. {
  264. $connection = ConnectionManager::get('test');
  265. $this->_createTables($connection);
  266. $schema = new SchemaCollection($connection);
  267. $result = $schema->listTables();
  268. $this->assertIsArray($result);
  269. $this->assertContains('schema_articles', $result);
  270. $this->assertContains('schema_authors', $result);
  271. $this->assertContains('view_schema_articles', $result);
  272. $resultNoViews = $schema->listTablesWithoutViews();
  273. $this->assertIsArray($resultNoViews);
  274. $this->assertContains('schema_authors', $resultNoViews);
  275. $this->assertContains('schema_articles', $resultNoViews);
  276. $this->assertNotContains('view_schema_articles', $resultNoViews);
  277. }
  278. /**
  279. * Test describing a table with Sqlite
  280. */
  281. public function testDescribeTable(): void
  282. {
  283. $connection = ConnectionManager::get('test');
  284. $this->_createTables($connection);
  285. $schema = new SchemaCollection($connection);
  286. $result = $schema->describe('schema_articles');
  287. $expected = [
  288. 'id' => [
  289. 'type' => 'integer',
  290. 'null' => false,
  291. 'default' => null,
  292. 'length' => null,
  293. 'precision' => null,
  294. 'comment' => null,
  295. 'unsigned' => false,
  296. 'autoIncrement' => true,
  297. ],
  298. 'title' => [
  299. 'type' => 'string',
  300. 'null' => true,
  301. 'default' => 'Let \'em eat cake',
  302. 'length' => 20,
  303. 'precision' => null,
  304. 'comment' => null,
  305. 'collate' => null,
  306. ],
  307. 'body' => [
  308. 'type' => 'text',
  309. 'null' => true,
  310. 'default' => null,
  311. 'length' => null,
  312. 'precision' => null,
  313. 'comment' => null,
  314. 'collate' => null,
  315. ],
  316. 'author_id' => [
  317. 'type' => 'integer',
  318. 'null' => false,
  319. 'default' => null,
  320. 'length' => 11,
  321. 'unsigned' => false,
  322. 'precision' => null,
  323. 'comment' => null,
  324. 'autoIncrement' => null,
  325. ],
  326. 'published' => [
  327. 'type' => 'boolean',
  328. 'null' => true,
  329. 'default' => 0,
  330. 'length' => null,
  331. 'precision' => null,
  332. 'comment' => null,
  333. ],
  334. 'created' => [
  335. 'type' => 'datetime',
  336. 'null' => true,
  337. 'default' => null,
  338. 'length' => null,
  339. 'precision' => null,
  340. 'comment' => null,
  341. ],
  342. 'field1' => [
  343. 'type' => 'string',
  344. 'null' => true,
  345. 'default' => null,
  346. 'length' => 10,
  347. 'precision' => null,
  348. 'comment' => null,
  349. 'collate' => null,
  350. ],
  351. 'field2' => [
  352. 'type' => 'string',
  353. 'null' => true,
  354. 'default' => 'NULL',
  355. 'length' => 10,
  356. 'precision' => null,
  357. 'comment' => null,
  358. 'collate' => null,
  359. ],
  360. ];
  361. $this->assertInstanceOf('Cake\Database\Schema\TableSchema', $result);
  362. $this->assertEquals(['id'], $result->getPrimaryKey());
  363. foreach ($expected as $field => $definition) {
  364. $this->assertEquals($definition, $result->getColumn($field));
  365. }
  366. }
  367. /**
  368. * Tests SQLite views
  369. */
  370. public function testDescribeView(): void
  371. {
  372. $connection = ConnectionManager::get('test');
  373. $this->_createTables($connection);
  374. $schema = new SchemaCollection($connection);
  375. $result = $schema->describe('view_schema_articles');
  376. $expected = [
  377. 'total' => [
  378. 'type' => 'text',
  379. 'length' => null,
  380. 'null' => true,
  381. 'default' => null,
  382. 'precision' => null,
  383. 'comment' => null,
  384. 'collate' => null,
  385. ],
  386. ];
  387. $this->assertInstanceOf('Cake\Database\Schema\TableSchema', $result);
  388. foreach ($expected as $field => $definition) {
  389. $this->assertSame($definition, $result->getColumn($field));
  390. }
  391. }
  392. /**
  393. * Test describing a table with Sqlite and composite keys
  394. *
  395. * Composite keys in SQLite are never autoincrement, and shouldn't be marked
  396. * as such.
  397. */
  398. public function testDescribeTableCompositeKey(): void
  399. {
  400. $connection = ConnectionManager::get('test');
  401. $this->_createTables($connection);
  402. $schema = new SchemaCollection($connection);
  403. $result = $schema->describe('schema_composite');
  404. $this->assertEquals(['id', 'site_id'], $result->getPrimaryKey());
  405. $this->assertNull($result->getColumn('site_id')['autoIncrement'], 'site_id should not be autoincrement');
  406. $this->assertNull($result->getColumn('id')['autoIncrement'], 'id should not be autoincrement');
  407. }
  408. /**
  409. * Test describing a table with indexes
  410. */
  411. public function testDescribeTableIndexes(): void
  412. {
  413. $connection = ConnectionManager::get('test');
  414. $this->_createTables($connection);
  415. $schema = new SchemaCollection($connection);
  416. $result = $schema->describe('schema_articles');
  417. $this->assertInstanceOf('Cake\Database\Schema\TableSchema', $result);
  418. $expected = [
  419. 'primary' => [
  420. 'type' => 'primary',
  421. 'columns' => ['id'],
  422. 'length' => [],
  423. ],
  424. 'sqlite_autoindex_schema_articles_1' => [
  425. 'type' => 'unique',
  426. 'columns' => ['title', 'body'],
  427. 'length' => [],
  428. ],
  429. 'author_id_fk' => [
  430. 'type' => 'foreign',
  431. 'columns' => ['author_id'],
  432. 'references' => ['schema_authors', 'id'],
  433. 'length' => [],
  434. 'update' => 'cascade',
  435. 'delete' => 'restrict',
  436. ],
  437. ];
  438. $this->assertCount(3, $result->constraints());
  439. $this->assertEquals($expected['primary'], $result->getConstraint('primary'));
  440. $this->assertEquals(
  441. $expected['sqlite_autoindex_schema_articles_1'],
  442. $result->getConstraint('sqlite_autoindex_schema_articles_1')
  443. );
  444. $this->assertEquals(
  445. $expected['author_id_fk'],
  446. $result->getConstraint('author_id_fk')
  447. );
  448. $this->assertCount(1, $result->indexes());
  449. $expected = [
  450. 'type' => 'index',
  451. 'columns' => ['created'],
  452. 'length' => [],
  453. ];
  454. $this->assertEquals($expected, $result->getIndex('created_idx'));
  455. }
  456. /**
  457. * Column provider for creating column sql
  458. *
  459. * @return array
  460. */
  461. public static function columnSqlProvider(): array
  462. {
  463. return [
  464. // strings
  465. [
  466. 'title',
  467. ['type' => 'string', 'length' => 25, 'null' => false],
  468. '"title" VARCHAR(25) NOT NULL',
  469. ],
  470. [
  471. 'title',
  472. ['type' => 'string', 'length' => 25, 'null' => true, 'default' => 'ignored'],
  473. '"title" VARCHAR(25) DEFAULT "ignored"',
  474. ],
  475. [
  476. 'id',
  477. ['type' => 'string', 'length' => 32, 'null' => false],
  478. '"id" VARCHAR(32) NOT NULL',
  479. ],
  480. [
  481. 'role',
  482. ['type' => 'string', 'length' => 10, 'null' => false, 'default' => 'admin'],
  483. '"role" VARCHAR(10) NOT NULL DEFAULT "admin"',
  484. ],
  485. [
  486. 'title',
  487. ['type' => 'string'],
  488. '"title" VARCHAR',
  489. ],
  490. [
  491. 'id',
  492. ['type' => 'uuid'],
  493. '"id" CHAR(36)',
  494. ],
  495. [
  496. 'id',
  497. ['type' => 'binaryuuid'],
  498. '"id" BINARY(16)',
  499. ],
  500. // Text
  501. [
  502. 'body',
  503. ['type' => 'text', 'null' => false],
  504. '"body" TEXT NOT NULL',
  505. ],
  506. [
  507. 'body',
  508. ['type' => 'text', 'length' => TableSchema::LENGTH_TINY, 'null' => false],
  509. '"body" VARCHAR(' . TableSchema::LENGTH_TINY . ') NOT NULL',
  510. ],
  511. [
  512. 'body',
  513. ['type' => 'text', 'length' => TableSchema::LENGTH_MEDIUM, 'null' => false],
  514. '"body" TEXT NOT NULL',
  515. ],
  516. [
  517. 'body',
  518. ['type' => 'text', 'length' => TableSchema::LENGTH_LONG, 'null' => false],
  519. '"body" TEXT NOT NULL',
  520. ],
  521. // Integers
  522. [
  523. 'post_id',
  524. ['type' => 'smallinteger', 'length' => 5, 'unsigned' => false],
  525. '"post_id" SMALLINT(5)',
  526. ],
  527. [
  528. 'post_id',
  529. ['type' => 'smallinteger', 'length' => 5, 'unsigned' => true],
  530. '"post_id" UNSIGNED SMALLINT(5)',
  531. ],
  532. [
  533. 'post_id',
  534. ['type' => 'tinyinteger', 'length' => 3, 'unsigned' => false],
  535. '"post_id" TINYINT(3)',
  536. ],
  537. [
  538. 'post_id',
  539. ['type' => 'tinyinteger', 'length' => 3, 'unsigned' => true],
  540. '"post_id" UNSIGNED TINYINT(3)',
  541. ],
  542. [
  543. 'post_id',
  544. ['type' => 'integer', 'length' => 11, 'unsigned' => false],
  545. '"post_id" INTEGER(11)',
  546. ],
  547. [
  548. 'post_id',
  549. ['type' => 'biginteger', 'length' => 20, 'unsigned' => false],
  550. '"post_id" BIGINT',
  551. ],
  552. [
  553. 'post_id',
  554. ['type' => 'biginteger', 'length' => 20, 'unsigned' => true],
  555. '"post_id" UNSIGNED BIGINT',
  556. ],
  557. // Decimal
  558. [
  559. 'value',
  560. ['type' => 'decimal', 'unsigned' => false],
  561. '"value" DECIMAL',
  562. ],
  563. [
  564. 'value',
  565. ['type' => 'decimal', 'length' => 11, 'unsigned' => false],
  566. '"value" DECIMAL(11,0)',
  567. ],
  568. [
  569. 'value',
  570. ['type' => 'decimal', 'length' => 11, 'unsigned' => true],
  571. '"value" UNSIGNED DECIMAL(11,0)',
  572. ],
  573. [
  574. 'value',
  575. ['type' => 'decimal', 'length' => 12, 'precision' => 5, 'unsigned' => false],
  576. '"value" DECIMAL(12,5)',
  577. ],
  578. // Float
  579. [
  580. 'value',
  581. ['type' => 'float'],
  582. '"value" FLOAT',
  583. ],
  584. [
  585. 'value',
  586. ['type' => 'float', 'length' => 11, 'precision' => 3, 'unsigned' => false],
  587. '"value" FLOAT(11,3)',
  588. ],
  589. [
  590. 'value',
  591. ['type' => 'float', 'length' => 11, 'precision' => 3, 'unsigned' => true],
  592. '"value" UNSIGNED FLOAT(11,3)',
  593. ],
  594. // Boolean
  595. [
  596. 'checked',
  597. ['type' => 'boolean', 'null' => true, 'default' => false],
  598. '"checked" BOOLEAN DEFAULT FALSE',
  599. ],
  600. [
  601. 'checked',
  602. ['type' => 'boolean', 'default' => true, 'null' => false],
  603. '"checked" BOOLEAN NOT NULL DEFAULT TRUE',
  604. ],
  605. // datetimes
  606. [
  607. 'created',
  608. ['type' => 'datetime'],
  609. '"created" DATETIME',
  610. ],
  611. [
  612. 'open_date',
  613. ['type' => 'datetime', 'null' => false, 'default' => '2016-12-07 23:04:00'],
  614. '"open_date" DATETIME NOT NULL DEFAULT "2016-12-07 23:04:00"',
  615. ],
  616. // Date & Time
  617. [
  618. 'start_date',
  619. ['type' => 'date'],
  620. '"start_date" DATE',
  621. ],
  622. [
  623. 'start_time',
  624. ['type' => 'time'],
  625. '"start_time" TIME',
  626. ],
  627. // timestamps
  628. [
  629. 'created',
  630. ['type' => 'timestamp', 'null' => true],
  631. '"created" TIMESTAMP DEFAULT NULL',
  632. ],
  633. ];
  634. }
  635. /**
  636. * Test the addConstraintSql method.
  637. */
  638. public function testAddConstraintSql(): void
  639. {
  640. $driver = $this->_getMockedDriver();
  641. $connection = $this->getMockBuilder('Cake\Database\Connection')
  642. ->disableOriginalConstructor()
  643. ->getMock();
  644. $connection->expects($this->any())->method('getDriver')
  645. ->will($this->returnValue($driver));
  646. $table = new TableSchema('posts');
  647. $result = $table->addConstraintSql($connection);
  648. $this->assertEmpty($result);
  649. }
  650. /**
  651. * Test the dropConstraintSql method.
  652. */
  653. public function testDropConstraintSql(): void
  654. {
  655. $driver = $this->_getMockedDriver();
  656. $connection = $this->getMockBuilder('Cake\Database\Connection')
  657. ->disableOriginalConstructor()
  658. ->getMock();
  659. $connection->expects($this->any())->method('getDriver')
  660. ->will($this->returnValue($driver));
  661. $table = new TableSchema('posts');
  662. $result = $table->dropConstraintSql($connection);
  663. $this->assertEmpty($result);
  664. }
  665. /**
  666. * Test generating column definitions
  667. *
  668. * @dataProvider columnSqlProvider
  669. */
  670. public function testColumnSql(string $name, array $data, string $expected): void
  671. {
  672. $driver = $this->_getMockedDriver();
  673. $schema = new SqliteSchemaDialect($driver);
  674. $table = (new TableSchema('articles'))->addColumn($name, $data);
  675. $this->assertEquals($expected, $schema->columnSql($table, $name));
  676. }
  677. /**
  678. * Test generating a column that is a primary key.
  679. */
  680. public function testColumnSqlPrimaryKey(): void
  681. {
  682. $driver = $this->_getMockedDriver();
  683. $schema = new SqliteSchemaDialect($driver);
  684. $table = new TableSchema('articles');
  685. $table->addColumn('id', [
  686. 'type' => 'integer',
  687. 'null' => false,
  688. 'length' => 11,
  689. 'unsigned' => true,
  690. ])
  691. ->addConstraint('primary', [
  692. 'type' => 'primary',
  693. 'columns' => ['id'],
  694. ]);
  695. $result = $schema->columnSql($table, 'id');
  696. $this->assertSame($result, '"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT');
  697. $result = $schema->constraintSql($table, 'primary');
  698. $this->assertSame('', $result, 'Integer primary keys are special in sqlite.');
  699. }
  700. /**
  701. * Test generating a bigint column that is a primary key.
  702. */
  703. public function testColumnSqlPrimaryKeyBigInt(): void
  704. {
  705. $driver = $this->_getMockedDriver();
  706. $schema = new SqliteSchemaDialect($driver);
  707. $table = new TableSchema('articles');
  708. $table->addColumn('id', [
  709. 'type' => 'biginteger',
  710. 'null' => false,
  711. ])
  712. ->addConstraint('primary', [
  713. 'type' => 'primary',
  714. 'columns' => ['id'],
  715. ]);
  716. $result = $schema->columnSql($table, 'id');
  717. $this->assertSame($result, '"id" BIGINT NOT NULL');
  718. $result = $schema->constraintSql($table, 'primary');
  719. $this->assertSame('CONSTRAINT "primary" PRIMARY KEY ("id")', $result, 'Bigint primary keys are not special.');
  720. }
  721. /**
  722. * Provide data for testing constraintSql
  723. *
  724. * @return array
  725. */
  726. public static function constraintSqlProvider(): array
  727. {
  728. return [
  729. [
  730. 'primary',
  731. ['type' => 'primary', 'columns' => ['title']],
  732. 'CONSTRAINT "primary" PRIMARY KEY ("title")',
  733. ],
  734. [
  735. 'unique_idx',
  736. ['type' => 'unique', 'columns' => ['title', 'author_id']],
  737. 'CONSTRAINT "unique_idx" UNIQUE ("title", "author_id")',
  738. ],
  739. [
  740. 'author_id_idx',
  741. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id']],
  742. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  743. 'REFERENCES "authors" ("id") ON UPDATE RESTRICT ON DELETE RESTRICT',
  744. ],
  745. [
  746. 'author_id_idx',
  747. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'cascade'],
  748. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  749. 'REFERENCES "authors" ("id") ON UPDATE CASCADE ON DELETE RESTRICT',
  750. ],
  751. [
  752. 'author_id_idx',
  753. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'restrict'],
  754. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  755. 'REFERENCES "authors" ("id") ON UPDATE RESTRICT ON DELETE RESTRICT',
  756. ],
  757. [
  758. 'author_id_idx',
  759. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'setNull'],
  760. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  761. 'REFERENCES "authors" ("id") ON UPDATE SET NULL ON DELETE RESTRICT',
  762. ],
  763. [
  764. 'author_id_idx',
  765. ['type' => 'foreign', 'columns' => ['author_id'], 'references' => ['authors', 'id'], 'update' => 'noAction'],
  766. 'CONSTRAINT "author_id_idx" FOREIGN KEY ("author_id") ' .
  767. 'REFERENCES "authors" ("id") ON UPDATE NO ACTION ON DELETE RESTRICT',
  768. ],
  769. ];
  770. }
  771. /**
  772. * Test the constraintSql method.
  773. *
  774. * @dataProvider constraintSqlProvider
  775. */
  776. public function testConstraintSql(string $name, array $data, string $expected): void
  777. {
  778. $driver = $this->_getMockedDriver();
  779. $schema = new SqliteSchemaDialect($driver);
  780. $table = (new TableSchema('articles'))->addColumn('title', [
  781. 'type' => 'string',
  782. 'length' => 255,
  783. ])->addColumn('author_id', [
  784. 'type' => 'integer',
  785. ])->addConstraint($name, $data);
  786. $this->assertEquals($expected, $schema->constraintSql($table, $name));
  787. }
  788. /**
  789. * Provide data for testing indexSql
  790. *
  791. * @return array
  792. */
  793. public static function indexSqlProvider(): array
  794. {
  795. return [
  796. [
  797. 'author_idx',
  798. ['type' => 'index', 'columns' => ['title', 'author_id']],
  799. 'CREATE INDEX "author_idx" ON "articles" ("title", "author_id")',
  800. ],
  801. ];
  802. }
  803. /**
  804. * Test the indexSql method.
  805. *
  806. * @dataProvider indexSqlProvider
  807. */
  808. public function testIndexSql(string $name, array $data, string $expected): void
  809. {
  810. $driver = $this->_getMockedDriver();
  811. $schema = new SqliteSchemaDialect($driver);
  812. $table = (new TableSchema('articles'))->addColumn('title', [
  813. 'type' => 'string',
  814. 'length' => 255,
  815. ])->addColumn('author_id', [
  816. 'type' => 'integer',
  817. ])->addIndex($name, $data);
  818. $this->assertEquals($expected, $schema->indexSql($table, $name));
  819. }
  820. /**
  821. * Integration test for converting a Schema\Table into MySQL table creates.
  822. */
  823. public function testCreateSql(): void
  824. {
  825. $driver = $this->_getMockedDriver();
  826. $connection = $this->getMockBuilder('Cake\Database\Connection')
  827. ->disableOriginalConstructor()
  828. ->getMock();
  829. $connection->expects($this->any())->method('getDriver')
  830. ->will($this->returnValue($driver));
  831. $table = (new TableSchema('articles'))->addColumn('id', [
  832. 'type' => 'integer',
  833. 'null' => false,
  834. ])
  835. ->addColumn('title', [
  836. 'type' => 'string',
  837. 'null' => false,
  838. ])
  839. ->addColumn('body', ['type' => 'text'])
  840. ->addColumn('data', ['type' => 'json'])
  841. ->addColumn('created', 'datetime')
  842. ->addConstraint('primary', [
  843. 'type' => 'primary',
  844. 'columns' => ['id'],
  845. ])
  846. ->addIndex('title_idx', [
  847. 'type' => 'index',
  848. 'columns' => ['title'],
  849. ]);
  850. $expected = <<<SQL
  851. CREATE TABLE "articles" (
  852. "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  853. "title" VARCHAR NOT NULL,
  854. "body" TEXT,
  855. "data" TEXT,
  856. "created" DATETIME
  857. )
  858. SQL;
  859. $result = $table->createSql($connection);
  860. $this->assertCount(2, $result);
  861. $this->assertTextEquals($expected, $result[0]);
  862. $this->assertSame(
  863. 'CREATE INDEX "title_idx" ON "articles" ("title")',
  864. $result[1]
  865. );
  866. }
  867. /**
  868. * Tests creating temporary tables
  869. */
  870. public function testCreateTemporary(): void
  871. {
  872. $driver = $this->_getMockedDriver();
  873. $connection = $this->getMockBuilder('Cake\Database\Connection')
  874. ->disableOriginalConstructor()
  875. ->getMock();
  876. $connection->expects($this->any())->method('getDriver')
  877. ->will($this->returnValue($driver));
  878. $table = (new TableSchema('schema_articles'))->addColumn('id', [
  879. 'type' => 'integer',
  880. 'null' => false,
  881. ]);
  882. $table->setTemporary(true);
  883. $sql = $table->createSql($connection);
  884. $this->assertStringContainsString('CREATE TEMPORARY TABLE', $sql[0]);
  885. }
  886. /**
  887. * Test primary key generation & auto-increment.
  888. */
  889. public function testCreateSqlCompositeIntegerKey(): void
  890. {
  891. $driver = $this->_getMockedDriver();
  892. $connection = $this->getMockBuilder('Cake\Database\Connection')
  893. ->disableOriginalConstructor()
  894. ->getMock();
  895. $connection->expects($this->any())->method('getDriver')
  896. ->will($this->returnValue($driver));
  897. $table = (new TableSchema('articles_tags'))
  898. ->addColumn('article_id', [
  899. 'type' => 'integer',
  900. 'null' => false,
  901. ])
  902. ->addColumn('tag_id', [
  903. 'type' => 'integer',
  904. 'null' => false,
  905. ])
  906. ->addConstraint('primary', [
  907. 'type' => 'primary',
  908. 'columns' => ['article_id', 'tag_id'],
  909. ]);
  910. $expected = <<<SQL
  911. CREATE TABLE "articles_tags" (
  912. "article_id" INTEGER NOT NULL,
  913. "tag_id" INTEGER NOT NULL,
  914. CONSTRAINT "primary" PRIMARY KEY ("article_id", "tag_id")
  915. )
  916. SQL;
  917. $result = $table->createSql($connection);
  918. $this->assertCount(1, $result);
  919. $this->assertTextEquals($expected, $result[0]);
  920. // Sqlite only supports AUTO_INCREMENT on single column primary
  921. // keys. Ensure that schema data follows the limitations of Sqlite.
  922. $table = (new TableSchema('composite_key'))
  923. ->addColumn('id', [
  924. 'type' => 'integer',
  925. 'null' => false,
  926. 'autoIncrement' => true,
  927. ])
  928. ->addColumn('account_id', [
  929. 'type' => 'integer',
  930. 'null' => false,
  931. ])
  932. ->addConstraint('primary', [
  933. 'type' => 'primary',
  934. 'columns' => ['id', 'account_id'],
  935. ]);
  936. $expected = <<<SQL
  937. CREATE TABLE "composite_key" (
  938. "id" INTEGER NOT NULL,
  939. "account_id" INTEGER NOT NULL,
  940. CONSTRAINT "primary" PRIMARY KEY ("id", "account_id")
  941. )
  942. SQL;
  943. $result = $table->createSql($connection);
  944. $this->assertCount(1, $result);
  945. $this->assertTextEquals($expected, $result[0]);
  946. }
  947. /**
  948. * test dropSql
  949. */
  950. public function testDropSql(): void
  951. {
  952. $driver = $this->_getMockedDriver();
  953. $connection = $this->getMockBuilder('Cake\Database\Connection')
  954. ->disableOriginalConstructor()
  955. ->getMock();
  956. $connection->expects($this->any())->method('getDriver')
  957. ->will($this->returnValue($driver));
  958. $table = new TableSchema('articles');
  959. $result = $table->dropSql($connection);
  960. $this->assertCount(1, $result);
  961. $this->assertSame('DROP TABLE "articles"', $result[0]);
  962. }
  963. /**
  964. * Test truncateSql()
  965. */
  966. public function testTruncateSql(): void
  967. {
  968. $driver = $this->_getMockedDriver();
  969. $connection = $this->getMockBuilder('Cake\Database\Connection')
  970. ->disableOriginalConstructor()
  971. ->getMock();
  972. $connection->expects($this->any())->method('getDriver')
  973. ->will($this->returnValue($driver));
  974. $statement = $this->getMockBuilder('\PDOStatement')
  975. ->onlyMethods(['execute', 'rowCount', 'closeCursor', 'fetchAll'])
  976. ->getMock();
  977. $driver->getConnection()->expects($this->once())
  978. ->method('prepare')
  979. ->with('SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"')
  980. ->will($this->returnValue($statement));
  981. $statement->expects($this->once())
  982. ->method('fetchAll')
  983. ->will($this->returnValue(['1']));
  984. $statement->method('execute')->will($this->returnValue(true));
  985. $table = new TableSchema('articles');
  986. $result = $table->truncateSql($connection);
  987. $this->assertCount(2, $result);
  988. $this->assertSame('DELETE FROM sqlite_sequence WHERE name="articles"', $result[0]);
  989. $this->assertSame('DELETE FROM "articles"', $result[1]);
  990. }
  991. /**
  992. * Test truncateSql() with no sequences
  993. */
  994. public function testTruncateSqlNoSequences(): void
  995. {
  996. $driver = $this->_getMockedDriver();
  997. $connection = $this->getMockBuilder('Cake\Database\Connection')
  998. ->disableOriginalConstructor()
  999. ->getMock();
  1000. $connection->expects($this->any())->method('getDriver')
  1001. ->will($this->returnValue($driver));
  1002. $statement = $this->getMockBuilder('\PDOStatement')
  1003. ->onlyMethods(['execute', 'rowCount', 'closeCursor', 'fetchAll'])
  1004. ->getMock();
  1005. $driver->getConnection()
  1006. ->expects($this->once())
  1007. ->method('prepare')
  1008. ->with('SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"')
  1009. ->will($this->returnValue($statement));
  1010. $statement->expects($this->once())
  1011. ->method('fetchAll')
  1012. ->will($this->returnValue([]));
  1013. $statement->method('execute')->will($this->returnValue(true));
  1014. $table = new TableSchema('articles');
  1015. $result = $table->truncateSql($connection);
  1016. $this->assertCount(1, $result);
  1017. $this->assertSame('DELETE FROM "articles"', $result[0]);
  1018. }
  1019. /**
  1020. * Get a schema instance with a mocked driver/pdo instances
  1021. */
  1022. protected function _getMockedDriver(): Driver
  1023. {
  1024. $driver = new Sqlite();
  1025. $mock = $this->getMockBuilder(PDO::class)
  1026. ->onlyMethods(['quote', 'prepare'])
  1027. ->disableOriginalConstructor()
  1028. ->getMock();
  1029. $mock->expects($this->any())
  1030. ->method('quote')
  1031. ->will($this->returnCallback(function ($value) {
  1032. return '"' . $value . '"';
  1033. }));
  1034. $driver->setConnection($mock);
  1035. return $driver;
  1036. }
  1037. }