SqlserverSchemaTest.php 38 KB

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