Schema.php 17 KB

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