You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

470 lines
18KB

  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\db\Connection;
  9. use yii\db\Exception;
  10. use yii\base\InvalidParamException;
  11. use yii\base\NotSupportedException;
  12. use yii\db\Expression;
  13. use yii\db\Query;
  14. /**
  15. * QueryBuilder is the query builder for SQLite databases.
  16. *
  17. * @author Qiang Xue <qiang.xue@gmail.com>
  18. * @since 2.0
  19. */
  20. class QueryBuilder extends \yii\db\QueryBuilder
  21. {
  22. /**
  23. * @var array mapping from abstract column types (keys) to physical column types (values).
  24. */
  25. public $typeMap = [
  26. Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
  27. Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
  28. Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
  29. Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
  30. Schema::TYPE_CHAR => 'char(1)',
  31. Schema::TYPE_STRING => 'varchar(255)',
  32. Schema::TYPE_TEXT => 'text',
  33. Schema::TYPE_SMALLINT => 'smallint',
  34. Schema::TYPE_INTEGER => 'integer',
  35. Schema::TYPE_BIGINT => 'bigint',
  36. Schema::TYPE_FLOAT => 'float',
  37. Schema::TYPE_DOUBLE => 'double',
  38. Schema::TYPE_DECIMAL => 'decimal(10,0)',
  39. Schema::TYPE_DATETIME => 'datetime',
  40. Schema::TYPE_TIMESTAMP => 'timestamp',
  41. Schema::TYPE_TIME => 'time',
  42. Schema::TYPE_DATE => 'date',
  43. Schema::TYPE_BINARY => 'blob',
  44. Schema::TYPE_BOOLEAN => 'boolean',
  45. Schema::TYPE_MONEY => 'decimal(19,4)',
  46. ];
  47. /**
  48. * Generates a batch INSERT SQL statement.
  49. * For example,
  50. *
  51. * ```php
  52. * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
  53. * ['Tom', 30],
  54. * ['Jane', 20],
  55. * ['Linda', 25],
  56. * ])->execute();
  57. * ```
  58. *
  59. * Note that the values in each row must match the corresponding column names.
  60. *
  61. * @param string $table the table that new rows will be inserted into.
  62. * @param array $columns the column names
  63. * @param array $rows the rows to be batch inserted into the table
  64. * @return string the batch INSERT SQL statement
  65. */
  66. public function batchInsert($table, $columns, $rows)
  67. {
  68. if (empty($rows)) {
  69. return '';
  70. }
  71. // SQLite supports batch insert natively since 3.7.11
  72. // http://www.sqlite.org/releaselog/3_7_11.html
  73. $this->db->open(); // ensure pdo is not null
  74. if (version_compare($this->db->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.7.11', '>=')) {
  75. return parent::batchInsert($table, $columns, $rows);
  76. }
  77. $schema = $this->db->getSchema();
  78. if (($tableSchema = $schema->getTableSchema($table)) !== null) {
  79. $columnSchemas = $tableSchema->columns;
  80. } else {
  81. $columnSchemas = [];
  82. }
  83. $values = [];
  84. foreach ($rows as $row) {
  85. $vs = [];
  86. foreach ($row as $i => $value) {
  87. if (!is_array($value) && isset($columnSchemas[$columns[$i]])) {
  88. $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
  89. }
  90. if (is_string($value)) {
  91. $value = $schema->quoteValue($value);
  92. } elseif ($value === false) {
  93. $value = 0;
  94. } elseif ($value === null) {
  95. $value = 'NULL';
  96. }
  97. $vs[] = $value;
  98. }
  99. $values[] = implode(', ', $vs);
  100. }
  101. foreach ($columns as $i => $name) {
  102. $columns[$i] = $schema->quoteColumnName($name);
  103. }
  104. return 'INSERT INTO ' . $schema->quoteTableName($table)
  105. . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
  106. }
  107. /**
  108. * Creates a SQL statement for resetting the sequence value of a table's primary key.
  109. * The sequence will be reset such that the primary key of the next new row inserted
  110. * will have the specified value or 1.
  111. * @param string $tableName the name of the table whose primary key sequence will be reset
  112. * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
  113. * the next new row's primary key will have a value 1.
  114. * @return string the SQL statement for resetting sequence
  115. * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table.
  116. */
  117. public function resetSequence($tableName, $value = null)
  118. {
  119. $db = $this->db;
  120. $table = $db->getTableSchema($tableName);
  121. if ($table !== null && $table->sequenceName !== null) {
  122. if ($value === null) {
  123. $key = reset($table->primaryKey);
  124. $tableName = $db->quoteTableName($tableName);
  125. $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) {
  126. return $db->createCommand("SELECT MAX('$key') FROM $tableName")->queryScalar();
  127. });
  128. } else {
  129. $value = (int) $value - 1;
  130. }
  131. try {
  132. $db->createCommand("UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'")->execute();
  133. } catch (Exception $e) {
  134. // it's possible that sqlite_sequence does not exist
  135. }
  136. } elseif ($table === null) {
  137. throw new InvalidParamException("Table not found: $tableName");
  138. } else {
  139. throw new InvalidParamException("There is not sequence associated with table '$tableName'.'");
  140. }
  141. }
  142. /**
  143. * Enables or disables integrity check.
  144. * @param boolean $check whether to turn on or off the integrity check.
  145. * @param string $schema the schema of the tables. Meaningless for SQLite.
  146. * @param string $table the table name. Meaningless for SQLite.
  147. * @return string the SQL statement for checking integrity
  148. * @throws NotSupportedException this is not supported by SQLite
  149. */
  150. public function checkIntegrity($check = true, $schema = '', $table = '')
  151. {
  152. return 'PRAGMA foreign_keys='.(int) $check;
  153. }
  154. /**
  155. * Builds a SQL statement for truncating a DB table.
  156. * @param string $table the table to be truncated. The name will be properly quoted by the method.
  157. * @return string the SQL statement for truncating a DB table.
  158. */
  159. public function truncateTable($table)
  160. {
  161. return 'DELETE FROM ' . $this->db->quoteTableName($table);
  162. }
  163. /**
  164. * Builds a SQL statement for dropping an index.
  165. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
  166. * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
  167. * @return string the SQL statement for dropping an index.
  168. */
  169. public function dropIndex($name, $table)
  170. {
  171. return 'DROP INDEX ' . $this->db->quoteTableName($name);
  172. }
  173. /**
  174. * Builds a SQL statement for dropping a DB column.
  175. * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
  176. * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
  177. * @return string the SQL statement for dropping a DB column.
  178. * @throws NotSupportedException this is not supported by SQLite
  179. */
  180. public function dropColumn($table, $column)
  181. {
  182. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  183. }
  184. /**
  185. * Builds a SQL statement for renaming a column.
  186. * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
  187. * @param string $oldName the old name of the column. The name will be properly quoted by the method.
  188. * @param string $newName the new name of the column. The name will be properly quoted by the method.
  189. * @return string the SQL statement for renaming a DB column.
  190. * @throws NotSupportedException this is not supported by SQLite
  191. */
  192. public function renameColumn($table, $oldName, $newName)
  193. {
  194. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  195. }
  196. /**
  197. * Builds a SQL statement for adding a foreign key constraint to an existing table.
  198. * The method will properly quote the table and column names.
  199. * @param string $name the name of the foreign key constraint.
  200. * @param string $table the table that the foreign key constraint will be added to.
  201. * @param string|array $columns the name of the column to that the constraint will be added on.
  202. * If there are multiple columns, separate them with commas or use an array to represent them.
  203. * @param string $refTable the table that the foreign key references to.
  204. * @param string|array $refColumns the name of the column that the foreign key references to.
  205. * If there are multiple columns, separate them with commas or use an array to represent them.
  206. * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  207. * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  208. * @return string the SQL statement for adding a foreign key constraint to an existing table.
  209. * @throws NotSupportedException this is not supported by SQLite
  210. */
  211. public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
  212. {
  213. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  214. }
  215. /**
  216. * Builds a SQL statement for dropping a foreign key constraint.
  217. * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
  218. * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
  219. * @return string the SQL statement for dropping a foreign key constraint.
  220. * @throws NotSupportedException this is not supported by SQLite
  221. */
  222. public function dropForeignKey($name, $table)
  223. {
  224. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  225. }
  226. /**
  227. * Builds a SQL statement for renaming a DB table.
  228. *
  229. * @param string $table the table to be renamed. The name will be properly quoted by the method.
  230. * @param string $newName the new table name. The name will be properly quoted by the method.
  231. * @return string the SQL statement for renaming a DB table.
  232. */
  233. public function renameTable($table, $newName)
  234. {
  235. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
  236. }
  237. /**
  238. * Builds a SQL statement for changing the definition of a column.
  239. * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
  240. * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
  241. * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
  242. * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
  243. * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
  244. * will become 'varchar(255) not null'.
  245. * @return string the SQL statement for changing the definition of a column.
  246. * @throws NotSupportedException this is not supported by SQLite
  247. */
  248. public function alterColumn($table, $column, $type)
  249. {
  250. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  251. }
  252. /**
  253. * Builds a SQL statement for adding a primary key constraint to an existing table.
  254. * @param string $name the name of the primary key constraint.
  255. * @param string $table the table that the primary key constraint will be added to.
  256. * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
  257. * @return string the SQL statement for adding a primary key constraint to an existing table.
  258. * @throws NotSupportedException this is not supported by SQLite
  259. */
  260. public function addPrimaryKey($name, $table, $columns)
  261. {
  262. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  263. }
  264. /**
  265. * Builds a SQL statement for removing a primary key constraint to an existing table.
  266. * @param string $name the name of the primary key constraint to be removed.
  267. * @param string $table the table that the primary key constraint will be removed from.
  268. * @return string the SQL statement for removing a primary key constraint from an existing table.
  269. * @throws NotSupportedException this is not supported by SQLite
  270. */
  271. public function dropPrimaryKey($name, $table)
  272. {
  273. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  274. }
  275. /**
  276. * @inheritdoc
  277. * @throws NotSupportedException
  278. * @since 2.0.8
  279. */
  280. public function addCommentOnColumn($table, $column, $comment)
  281. {
  282. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  283. }
  284. /**
  285. * @inheritdoc
  286. * @throws NotSupportedException
  287. * @since 2.0.8
  288. */
  289. public function addCommentOnTable($table, $comment)
  290. {
  291. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  292. }
  293. /**
  294. * @inheritdoc
  295. * @throws NotSupportedException
  296. * @since 2.0.8
  297. */
  298. public function dropCommentFromColumn($table, $column)
  299. {
  300. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  301. }
  302. /**
  303. * @inheritdoc
  304. * @throws NotSupportedException
  305. * @since 2.0.8
  306. */
  307. public function dropCommentFromTable($table)
  308. {
  309. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  310. }
  311. /**
  312. * @inheritdoc
  313. */
  314. public function buildLimit($limit, $offset)
  315. {
  316. $sql = '';
  317. if ($this->hasLimit($limit)) {
  318. $sql = 'LIMIT ' . $limit;
  319. if ($this->hasOffset($offset)) {
  320. $sql .= ' OFFSET ' . $offset;
  321. }
  322. } elseif ($this->hasOffset($offset)) {
  323. // limit is not optional in SQLite
  324. // http://www.sqlite.org/syntaxdiagrams.html#select-stmt
  325. $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
  326. }
  327. return $sql;
  328. }
  329. /**
  330. * @inheritdoc
  331. * @throws NotSupportedException if `$columns` is an array
  332. */
  333. protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
  334. {
  335. if (is_array($columns)) {
  336. throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
  337. }
  338. return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
  339. }
  340. /**
  341. * Builds SQL for IN condition
  342. *
  343. * @param string $operator
  344. * @param array $columns
  345. * @param array $values
  346. * @param array $params
  347. * @return string SQL
  348. */
  349. protected function buildCompositeInCondition($operator, $columns, $values, &$params)
  350. {
  351. $quotedColumns = [];
  352. foreach ($columns as $i => $column) {
  353. $quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
  354. }
  355. $vss = [];
  356. foreach ($values as $value) {
  357. $vs = [];
  358. foreach ($columns as $i => $column) {
  359. if (isset($value[$column])) {
  360. $phName = self::PARAM_PREFIX . count($params);
  361. $params[$phName] = $value[$column];
  362. $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
  363. } else {
  364. $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
  365. }
  366. }
  367. $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
  368. }
  369. return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
  370. }
  371. /**
  372. * @inheritdoc
  373. */
  374. public function build($query, $params = [])
  375. {
  376. $query = $query->prepare($this);
  377. $params = empty($params) ? $query->params : array_merge($params, $query->params);
  378. $clauses = [
  379. $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
  380. $this->buildFrom($query->from, $params),
  381. $this->buildJoin($query->join, $params),
  382. $this->buildWhere($query->where, $params),
  383. $this->buildGroupBy($query->groupBy),
  384. $this->buildHaving($query->having, $params),
  385. ];
  386. $sql = implode($this->separator, array_filter($clauses));
  387. $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
  388. if (!empty($query->orderBy)) {
  389. foreach ($query->orderBy as $expression) {
  390. if ($expression instanceof Expression) {
  391. $params = array_merge($params, $expression->params);
  392. }
  393. }
  394. }
  395. if (!empty($query->groupBy)) {
  396. foreach ($query->groupBy as $expression) {
  397. if ($expression instanceof Expression) {
  398. $params = array_merge($params, $expression->params);
  399. }
  400. }
  401. }
  402. $union = $this->buildUnion($query->union, $params);
  403. if ($union !== '') {
  404. $sql = "$sql{$this->separator}$union";
  405. }
  406. return [$sql, $params];
  407. }
  408. /**
  409. * @inheritdoc
  410. */
  411. public function buildUnion($unions, &$params)
  412. {
  413. if (empty($unions)) {
  414. return '';
  415. }
  416. $result = '';
  417. foreach ($unions as $i => $union) {
  418. $query = $union['query'];
  419. if ($query instanceof Query) {
  420. list($unions[$i]['query'], $params) = $this->build($query, $params);
  421. }
  422. $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
  423. }
  424. return trim($result);
  425. }
  426. }