Schema.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\db\sqlite;
  8. use yii\base\NotSupportedException;
  9. use yii\db\CheckConstraint;
  10. use yii\db\ColumnSchema;
  11. use yii\db\Constraint;
  12. use yii\db\ConstraintFinderInterface;
  13. use yii\db\ConstraintFinderTrait;
  14. use yii\db\Expression;
  15. use yii\db\ForeignKeyConstraint;
  16. use yii\db\IndexConstraint;
  17. use yii\db\SqlToken;
  18. use yii\db\TableSchema;
  19. use yii\db\Transaction;
  20. use yii\helpers\ArrayHelper;
  21. /**
  22. * Schema is the class for retrieving metadata from a SQLite (2/3) database.
  23. *
  24. * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction.
  25. * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
  26. *
  27. * @author Qiang Xue <qiang.xue@gmail.com>
  28. * @since 2.0
  29. */
  30. class Schema extends \yii\db\Schema implements ConstraintFinderInterface
  31. {
  32. use ConstraintFinderTrait;
  33. /**
  34. * @var array mapping from physical column types (keys) to abstract column types (values)
  35. */
  36. public $typeMap = [
  37. 'tinyint' => self::TYPE_TINYINT,
  38. 'bit' => self::TYPE_SMALLINT,
  39. 'boolean' => self::TYPE_BOOLEAN,
  40. 'bool' => self::TYPE_BOOLEAN,
  41. 'smallint' => self::TYPE_SMALLINT,
  42. 'mediumint' => self::TYPE_INTEGER,
  43. 'int' => self::TYPE_INTEGER,
  44. 'integer' => self::TYPE_INTEGER,
  45. 'bigint' => self::TYPE_BIGINT,
  46. 'float' => self::TYPE_FLOAT,
  47. 'double' => self::TYPE_DOUBLE,
  48. 'real' => self::TYPE_FLOAT,
  49. 'decimal' => self::TYPE_DECIMAL,
  50. 'numeric' => self::TYPE_DECIMAL,
  51. 'tinytext' => self::TYPE_TEXT,
  52. 'mediumtext' => self::TYPE_TEXT,
  53. 'longtext' => self::TYPE_TEXT,
  54. 'text' => self::TYPE_TEXT,
  55. 'varchar' => self::TYPE_STRING,
  56. 'string' => self::TYPE_STRING,
  57. 'char' => self::TYPE_CHAR,
  58. 'blob' => self::TYPE_BINARY,
  59. 'datetime' => self::TYPE_DATETIME,
  60. 'year' => self::TYPE_DATE,
  61. 'date' => self::TYPE_DATE,
  62. 'time' => self::TYPE_TIME,
  63. 'timestamp' => self::TYPE_TIMESTAMP,
  64. 'enum' => self::TYPE_STRING,
  65. ];
  66. /**
  67. * {@inheritdoc}
  68. */
  69. protected $tableQuoteCharacter = '`';
  70. /**
  71. * {@inheritdoc}
  72. */
  73. protected $columnQuoteCharacter = '`';
  74. /**
  75. * {@inheritdoc}
  76. */
  77. protected function findTableNames($schema = '')
  78. {
  79. $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
  80. return $this->db->createCommand($sql)->queryColumn();
  81. }
  82. /**
  83. * {@inheritdoc}
  84. */
  85. protected function loadTableSchema($name)
  86. {
  87. $table = new TableSchema();
  88. $table->name = $name;
  89. $table->fullName = $name;
  90. if ($this->findColumns($table)) {
  91. $this->findConstraints($table);
  92. return $table;
  93. }
  94. return null;
  95. }
  96. /**
  97. * {@inheritdoc}
  98. */
  99. protected function loadTablePrimaryKey($tableName)
  100. {
  101. return $this->loadTableConstraints($tableName, 'primaryKey');
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. protected function loadTableForeignKeys($tableName)
  107. {
  108. $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
  109. $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
  110. $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
  111. ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
  112. $result = [];
  113. foreach ($foreignKeys as $table => $foreignKey) {
  114. $result[] = new ForeignKeyConstraint([
  115. 'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'),
  116. 'foreignTableName' => $table,
  117. 'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'),
  118. 'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null,
  119. 'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null,
  120. ]);
  121. }
  122. return $result;
  123. }
  124. /**
  125. * {@inheritdoc}
  126. */
  127. protected function loadTableIndexes($tableName)
  128. {
  129. return $this->loadTableConstraints($tableName, 'indexes');
  130. }
  131. /**
  132. * {@inheritdoc}
  133. */
  134. protected function loadTableUniques($tableName)
  135. {
  136. return $this->loadTableConstraints($tableName, 'uniques');
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. protected function loadTableChecks($tableName)
  142. {
  143. $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
  144. ':tableName' => $tableName,
  145. ])->queryScalar();
  146. /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
  147. $code = (new SqlTokenizer($sql))->tokenize();
  148. $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
  149. if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
  150. return [];
  151. }
  152. $createTableToken = $code[0][$lastMatchIndex - 1];
  153. $result = [];
  154. $offset = 0;
  155. while (true) {
  156. $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
  157. if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
  158. break;
  159. }
  160. $checkSql = $createTableToken[$offset - 1]->getSql();
  161. $name = null;
  162. $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
  163. if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
  164. $name = $createTableToken[$firstMatchIndex - 1]->content;
  165. }
  166. $result[] = new CheckConstraint([
  167. 'name' => $name,
  168. 'expression' => $checkSql,
  169. ]);
  170. }
  171. return $result;
  172. }
  173. /**
  174. * {@inheritdoc}
  175. * @throws NotSupportedException if this method is called.
  176. */
  177. protected function loadTableDefaultValues($tableName)
  178. {
  179. throw new NotSupportedException('SQLite does not support default value constraints.');
  180. }
  181. /**
  182. * Creates a query builder for the MySQL database.
  183. * This method may be overridden by child classes to create a DBMS-specific query builder.
  184. * @return QueryBuilder query builder instance
  185. */
  186. public function createQueryBuilder()
  187. {
  188. return new QueryBuilder($this->db);
  189. }
  190. /**
  191. * {@inheritdoc}
  192. * @return ColumnSchemaBuilder column schema builder instance
  193. */
  194. public function createColumnSchemaBuilder($type, $length = null)
  195. {
  196. return new ColumnSchemaBuilder($type, $length);
  197. }
  198. /**
  199. * Collects the table column metadata.
  200. * @param TableSchema $table the table metadata
  201. * @return bool whether the table exists in the database
  202. */
  203. protected function findColumns($table)
  204. {
  205. $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
  206. $columns = $this->db->createCommand($sql)->queryAll();
  207. if (empty($columns)) {
  208. return false;
  209. }
  210. foreach ($columns as $info) {
  211. $column = $this->loadColumnSchema($info);
  212. $table->columns[$column->name] = $column;
  213. if ($column->isPrimaryKey) {
  214. $table->primaryKey[] = $column->name;
  215. }
  216. }
  217. if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
  218. $table->sequenceName = '';
  219. $table->columns[$table->primaryKey[0]]->autoIncrement = true;
  220. }
  221. return true;
  222. }
  223. /**
  224. * Collects the foreign key column details for the given table.
  225. * @param TableSchema $table the table metadata
  226. */
  227. protected function findConstraints($table)
  228. {
  229. $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
  230. $keys = $this->db->createCommand($sql)->queryAll();
  231. foreach ($keys as $key) {
  232. $id = (int) $key['id'];
  233. if (!isset($table->foreignKeys[$id])) {
  234. $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
  235. } else {
  236. // composite FK
  237. $table->foreignKeys[$id][$key['from']] = $key['to'];
  238. }
  239. }
  240. }
  241. /**
  242. * Returns all unique indexes for the given table.
  243. *
  244. * Each array element is of the following structure:
  245. *
  246. * ```php
  247. * [
  248. * 'IndexName1' => ['col1' [, ...]],
  249. * 'IndexName2' => ['col2' [, ...]],
  250. * ]
  251. * ```
  252. *
  253. * @param TableSchema $table the table metadata
  254. * @return array all unique indexes for the given table.
  255. */
  256. public function findUniqueIndexes($table)
  257. {
  258. $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
  259. $indexes = $this->db->createCommand($sql)->queryAll();
  260. $uniqueIndexes = [];
  261. foreach ($indexes as $index) {
  262. $indexName = $index['name'];
  263. $indexInfo = $this->db->createCommand('PRAGMA index_info(' . $this->quoteValue($index['name']) . ')')->queryAll();
  264. if ($index['unique']) {
  265. $uniqueIndexes[$indexName] = [];
  266. foreach ($indexInfo as $row) {
  267. $uniqueIndexes[$indexName][] = $row['name'];
  268. }
  269. }
  270. }
  271. return $uniqueIndexes;
  272. }
  273. /**
  274. * Loads the column information into a [[ColumnSchema]] object.
  275. * @param array $info column information
  276. * @return ColumnSchema the column schema object
  277. */
  278. protected function loadColumnSchema($info)
  279. {
  280. $column = $this->createColumnSchema();
  281. $column->name = $info['name'];
  282. $column->allowNull = !$info['notnull'];
  283. $column->isPrimaryKey = $info['pk'] != 0;
  284. $column->dbType = strtolower($info['type']);
  285. $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
  286. $column->type = self::TYPE_STRING;
  287. if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
  288. $type = strtolower($matches[1]);
  289. if (isset($this->typeMap[$type])) {
  290. $column->type = $this->typeMap[$type];
  291. }
  292. if (!empty($matches[2])) {
  293. $values = explode(',', $matches[2]);
  294. $column->size = $column->precision = (int) $values[0];
  295. if (isset($values[1])) {
  296. $column->scale = (int) $values[1];
  297. }
  298. if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
  299. $column->type = 'boolean';
  300. } elseif ($type === 'bit') {
  301. if ($column->size > 32) {
  302. $column->type = 'bigint';
  303. } elseif ($column->size === 32) {
  304. $column->type = 'integer';
  305. }
  306. }
  307. }
  308. }
  309. $column->phpType = $this->getColumnPhpType($column);
  310. if (!$column->isPrimaryKey) {
  311. if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
  312. $column->defaultValue = null;
  313. } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
  314. $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
  315. } else {
  316. $value = trim($info['dflt_value'], "'\"");
  317. $column->defaultValue = $column->phpTypecast($value);
  318. }
  319. }
  320. return $column;
  321. }
  322. /**
  323. * Sets the isolation level of the current transaction.
  324. * @param string $level The transaction isolation level to use for this transaction.
  325. * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
  326. * @throws NotSupportedException when unsupported isolation levels are used.
  327. * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
  328. * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted
  329. */
  330. public function setTransactionIsolationLevel($level)
  331. {
  332. switch ($level) {
  333. case Transaction::SERIALIZABLE:
  334. $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
  335. break;
  336. case Transaction::READ_UNCOMMITTED:
  337. $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
  338. break;
  339. default:
  340. throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
  341. }
  342. }
  343. /**
  344. * Returns table columns info.
  345. * @param string $tableName table name
  346. * @return array
  347. */
  348. private function loadTableColumnsInfo($tableName)
  349. {
  350. $tableColumns = $this->db->createCommand('PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')')->queryAll();
  351. $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
  352. return ArrayHelper::index($tableColumns, 'cid');
  353. }
  354. /**
  355. * Loads multiple types of constraints and returns the specified ones.
  356. * @param string $tableName table name.
  357. * @param string $returnType return type:
  358. * - primaryKey
  359. * - indexes
  360. * - uniques
  361. * @return mixed constraints.
  362. */
  363. private function loadTableConstraints($tableName, $returnType)
  364. {
  365. $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
  366. $indexes = $this->normalizePdoRowKeyCase($indexes, true);
  367. $tableColumns = null;
  368. if (!empty($indexes) && !isset($indexes[0]['origin'])) {
  369. /*
  370. * SQLite may not have an "origin" column in INDEX_LIST
  371. * See https://www.sqlite.org/src/info/2743846cdba572f6
  372. */
  373. $tableColumns = $this->loadTableColumnsInfo($tableName);
  374. }
  375. $result = [
  376. 'primaryKey' => null,
  377. 'indexes' => [],
  378. 'uniques' => [],
  379. ];
  380. foreach ($indexes as $index) {
  381. $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll();
  382. $columns = $this->normalizePdoRowKeyCase($columns, true);
  383. ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
  384. if ($tableColumns !== null) {
  385. // SQLite may not have an "origin" column in INDEX_LIST
  386. $index['origin'] = 'c';
  387. if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
  388. $index['origin'] = 'pk';
  389. } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
  390. $index['origin'] = 'u';
  391. }
  392. }
  393. $result['indexes'][] = new IndexConstraint([
  394. 'isPrimary' => $index['origin'] === 'pk',
  395. 'isUnique' => (bool) $index['unique'],
  396. 'name' => $index['name'],
  397. 'columnNames' => ArrayHelper::getColumn($columns, 'name'),
  398. ]);
  399. if ($index['origin'] === 'u') {
  400. $result['uniques'][] = new Constraint([
  401. 'name' => $index['name'],
  402. 'columnNames' => ArrayHelper::getColumn($columns, 'name'),
  403. ]);
  404. } elseif ($index['origin'] === 'pk') {
  405. $result['primaryKey'] = new Constraint([
  406. 'columnNames' => ArrayHelper::getColumn($columns, 'name'),
  407. ]);
  408. }
  409. }
  410. if ($result['primaryKey'] === null) {
  411. /*
  412. * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
  413. * See https://www.sqlite.org/lang_createtable.html#primkeyconst
  414. */
  415. if ($tableColumns === null) {
  416. $tableColumns = $this->loadTableColumnsInfo($tableName);
  417. }
  418. foreach ($tableColumns as $tableColumn) {
  419. if ($tableColumn['pk'] > 0) {
  420. $result['primaryKey'] = new Constraint([
  421. 'columnNames' => [$tableColumn['name']],
  422. ]);
  423. break;
  424. }
  425. }
  426. }
  427. foreach ($result as $type => $data) {
  428. $this->setTableMetadata($tableName, $type, $data);
  429. }
  430. return $result[$returnType];
  431. }
  432. /**
  433. * Return whether the specified identifier is a SQLite system identifier.
  434. * @param string $identifier
  435. * @return bool
  436. * @see https://www.sqlite.org/src/artifact/74108007d286232f
  437. */
  438. private function isSystemIdentifier($identifier)
  439. {
  440. return strncmp($identifier, 'sqlite_', 7) === 0;
  441. }
  442. }