SqlserverSchemaTest.php 34 KB

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