QueryBuilder.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  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;
  8. use yii\base\InvalidParamException;
  9. use yii\base\NotSupportedException;
  10. /**
  11. * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
  12. *
  13. * QueryBuilder can also be used to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE,
  14. * from a [[Query]] object.
  15. *
  16. * @author Qiang Xue <[email protected]>
  17. * @since 2.0
  18. */
  19. class QueryBuilder extends \yii\base\Object
  20. {
  21. /**
  22. * The prefix for automatically generated query binding parameters.
  23. */
  24. const PARAM_PREFIX = ':qp';
  25. /**
  26. * @var Connection the database connection.
  27. */
  28. public $db;
  29. /**
  30. * @var string the separator between different fragments of a SQL statement.
  31. * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
  32. */
  33. public $separator = " ";
  34. /**
  35. * @var array the abstract column types mapped to physical column types.
  36. * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
  37. * Child classes should override this property to declare supported type mappings.
  38. */
  39. public $typeMap = [];
  40. /**
  41. * Constructor.
  42. * @param Connection $connection the database connection.
  43. * @param array $config name-value pairs that will be used to initialize the object properties
  44. */
  45. public function __construct($connection, $config = [])
  46. {
  47. $this->db = $connection;
  48. parent::__construct($config);
  49. }
  50. /**
  51. * Generates a SELECT SQL statement from a [[Query]] object.
  52. * @param Query $query the [[Query]] object from which the SQL statement will be generated
  53. * @return array the generated SQL statement (the first array element) and the corresponding
  54. * parameters to be bound to the SQL statement (the second array element).
  55. */
  56. public function build($query)
  57. {
  58. $params = $query->params;
  59. $clauses = [
  60. $this->buildSelect($query->select, $query->distinct, $query->selectOption),
  61. $this->buildFrom($query->from),
  62. $this->buildJoin($query->join, $params),
  63. $this->buildWhere($query->where, $params),
  64. $this->buildGroupBy($query->groupBy),
  65. $this->buildHaving($query->having, $params),
  66. $this->buildUnion($query->union, $params),
  67. $this->buildOrderBy($query->orderBy),
  68. $this->buildLimit($query->limit, $query->offset),
  69. ];
  70. return [implode($this->separator, array_filter($clauses)), $params];
  71. }
  72. /**
  73. * Creates an INSERT SQL statement.
  74. * For example,
  75. *
  76. * ~~~
  77. * $sql = $queryBuilder->insert('tbl_user', [
  78. * 'name' => 'Sam',
  79. * 'age' => 30,
  80. * ], $params);
  81. * ~~~
  82. *
  83. * The method will properly escape the table and column names.
  84. *
  85. * @param string $table the table that new rows will be inserted into.
  86. * @param array $columns the column data (name => value) to be inserted into the table.
  87. * @param array $params the binding parameters that will be generated by this method.
  88. * They should be bound to the DB command later.
  89. * @return string the INSERT SQL
  90. */
  91. public function insert($table, $columns, &$params)
  92. {
  93. if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
  94. $columnSchemas = $tableSchema->columns;
  95. } else {
  96. $columnSchemas = [];
  97. }
  98. $names = [];
  99. $placeholders = [];
  100. foreach ($columns as $name => $value) {
  101. $names[] = $this->db->quoteColumnName($name);
  102. if ($value instanceof Expression) {
  103. $placeholders[] = $value->expression;
  104. foreach ($value->params as $n => $v) {
  105. $params[$n] = $v;
  106. }
  107. } else {
  108. $phName = self::PARAM_PREFIX . count($params);
  109. $placeholders[] = $phName;
  110. $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value;
  111. }
  112. }
  113. return 'INSERT INTO ' . $this->db->quoteTableName($table)
  114. . ' (' . implode(', ', $names) . ') VALUES ('
  115. . implode(', ', $placeholders) . ')';
  116. }
  117. /**
  118. * Generates a batch INSERT SQL statement.
  119. * For example,
  120. *
  121. * ~~~
  122. * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [
  123. * ['Tom', 30],
  124. * ['Jane', 20],
  125. * ['Linda', 25],
  126. * ])->execute();
  127. * ~~~
  128. *
  129. * Note that the values in each row must match the corresponding column names.
  130. *
  131. * @param string $table the table that new rows will be inserted into.
  132. * @param array $columns the column names
  133. * @param array $rows the rows to be batch inserted into the table
  134. * @return string the batch INSERT SQL statement
  135. */
  136. public function batchInsert($table, $columns, $rows)
  137. {
  138. if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
  139. $columnSchemas = $tableSchema->columns;
  140. } else {
  141. $columnSchemas = [];
  142. }
  143. foreach ($columns as $i => $name) {
  144. $columns[$i] = $this->db->quoteColumnName($name);
  145. }
  146. $values = [];
  147. foreach ($rows as $row) {
  148. $vs = [];
  149. foreach ($row as $i => $value) {
  150. if (!is_array($value) && isset($columnSchemas[$columns[$i]])) {
  151. $value = $columnSchemas[$columns[$i]]->typecast($value);
  152. }
  153. $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value;
  154. }
  155. $values[] = '(' . implode(', ', $vs) . ')';
  156. }
  157. return 'INSERT INTO ' . $this->db->quoteTableName($table)
  158. . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
  159. }
  160. /**
  161. * Creates an UPDATE SQL statement.
  162. * For example,
  163. *
  164. * ~~~
  165. * $params = [];
  166. * $sql = $queryBuilder->update('tbl_user', ['status' => 1], 'age > 30', $params);
  167. * ~~~
  168. *
  169. * The method will properly escape the table and column names.
  170. *
  171. * @param string $table the table to be updated.
  172. * @param array $columns the column data (name => value) to be updated.
  173. * @param array|string $condition the condition that will be put in the WHERE part. Please
  174. * refer to [[Query::where()]] on how to specify condition.
  175. * @param array $params the binding parameters that will be modified by this method
  176. * so that they can be bound to the DB command later.
  177. * @return string the UPDATE SQL
  178. */
  179. public function update($table, $columns, $condition, &$params)
  180. {
  181. if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
  182. $columnSchemas = $tableSchema->columns;
  183. } else {
  184. $columnSchemas = [];
  185. }
  186. $lines = [];
  187. foreach ($columns as $name => $value) {
  188. if ($value instanceof Expression) {
  189. $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
  190. foreach ($value->params as $n => $v) {
  191. $params[$n] = $v;
  192. }
  193. } else {
  194. $phName = self::PARAM_PREFIX . count($params);
  195. $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
  196. $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value;
  197. }
  198. }
  199. $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
  200. $where = $this->buildWhere($condition, $params);
  201. return $where === '' ? $sql : $sql . ' ' . $where;
  202. }
  203. /**
  204. * Creates a DELETE SQL statement.
  205. * For example,
  206. *
  207. * ~~~
  208. * $sql = $queryBuilder->delete('tbl_user', 'status = 0');
  209. * ~~~
  210. *
  211. * The method will properly escape the table and column names.
  212. *
  213. * @param string $table the table where the data will be deleted from.
  214. * @param array|string $condition the condition that will be put in the WHERE part. Please
  215. * refer to [[Query::where()]] on how to specify condition.
  216. * @param array $params the binding parameters that will be modified by this method
  217. * so that they can be bound to the DB command later.
  218. * @return string the DELETE SQL
  219. */
  220. public function delete($table, $condition, &$params)
  221. {
  222. $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
  223. $where = $this->buildWhere($condition, $params);
  224. return $where === '' ? $sql : $sql . ' ' . $where;
  225. }
  226. /**
  227. * Builds a SQL statement for creating a new DB table.
  228. *
  229. * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
  230. * where name stands for a column name which will be properly quoted by the method, and definition
  231. * stands for the column type which can contain an abstract DB type.
  232. * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
  233. *
  234. * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
  235. * inserted into the generated SQL.
  236. *
  237. * For example,
  238. *
  239. * ~~~
  240. * $sql = $queryBuilder->createTable('tbl_user', [
  241. * 'id' => 'pk',
  242. * 'name' => 'string',
  243. * 'age' => 'integer',
  244. * ]);
  245. * ~~~
  246. *
  247. * @param string $table the name of the table to be created. The name will be properly quoted by the method.
  248. * @param array $columns the columns (name => definition) in the new table.
  249. * @param string $options additional SQL fragment that will be appended to the generated SQL.
  250. * @return string the SQL statement for creating a new DB table.
  251. */
  252. public function createTable($table, $columns, $options = null)
  253. {
  254. $cols = [];
  255. foreach ($columns as $name => $type) {
  256. if (is_string($name)) {
  257. $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
  258. } else {
  259. $cols[] = "\t" . $type;
  260. }
  261. }
  262. $sql = "CREATE TABLE " . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
  263. return $options === null ? $sql : $sql . ' ' . $options;
  264. }
  265. /**
  266. * Builds a SQL statement for renaming a DB table.
  267. * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
  268. * @param string $newName the new table name. The name will be properly quoted by the method.
  269. * @return string the SQL statement for renaming a DB table.
  270. */
  271. public function renameTable($oldName, $newName)
  272. {
  273. return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
  274. }
  275. /**
  276. * Builds a SQL statement for dropping a DB table.
  277. * @param string $table the table to be dropped. The name will be properly quoted by the method.
  278. * @return string the SQL statement for dropping a DB table.
  279. */
  280. public function dropTable($table)
  281. {
  282. return "DROP TABLE " . $this->db->quoteTableName($table);
  283. }
  284. /**
  285. * Builds a SQL statement for adding a primary key constraint to an existing table.
  286. * @param string $name the name of the primary key constraint.
  287. * @param string $table the table that the primary key constraint will be added to.
  288. * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
  289. * @return string the SQL statement for adding a primary key constraint to an existing table.
  290. */
  291. public function addPrimaryKey($name, $table, $columns)
  292. {
  293. if (is_string($columns)) {
  294. $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
  295. }
  296. foreach ($columns as $i => $col) {
  297. $columns[$i] = $this->db->quoteColumnName($col);
  298. }
  299. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
  300. . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
  301. . implode(', ', $columns). ' )';
  302. }
  303. /**
  304. * Builds a SQL statement for removing a primary key constraint to an existing table.
  305. * @param string $name the name of the primary key constraint to be removed.
  306. * @param string $table the table that the primary key constraint will be removed from.
  307. * @return string the SQL statement for removing a primary key constraint from an existing table. *
  308. */
  309. public function dropPrimaryKey($name, $table)
  310. {
  311. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  312. . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
  313. }
  314. /**
  315. * Builds a SQL statement for truncating a DB table.
  316. * @param string $table the table to be truncated. The name will be properly quoted by the method.
  317. * @return string the SQL statement for truncating a DB table.
  318. */
  319. public function truncateTable($table)
  320. {
  321. return "TRUNCATE TABLE " . $this->db->quoteTableName($table);
  322. }
  323. /**
  324. * Builds a SQL statement for adding a new DB column.
  325. * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
  326. * @param string $column the name of the new column. The name will be properly quoted by the method.
  327. * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
  328. * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
  329. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
  330. * @return string the SQL statement for adding a new column.
  331. */
  332. public function addColumn($table, $column, $type)
  333. {
  334. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  335. . ' ADD ' . $this->db->quoteColumnName($column) . ' '
  336. . $this->getColumnType($type);
  337. }
  338. /**
  339. * Builds a SQL statement for dropping a DB column.
  340. * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
  341. * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
  342. * @return string the SQL statement for dropping a DB column.
  343. */
  344. public function dropColumn($table, $column)
  345. {
  346. return "ALTER TABLE " . $this->db->quoteTableName($table)
  347. . " DROP COLUMN " . $this->db->quoteColumnName($column);
  348. }
  349. /**
  350. * Builds a SQL statement for renaming a column.
  351. * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
  352. * @param string $oldName the old name of the column. The name will be properly quoted by the method.
  353. * @param string $newName the new name of the column. The name will be properly quoted by the method.
  354. * @return string the SQL statement for renaming a DB column.
  355. */
  356. public function renameColumn($table, $oldName, $newName)
  357. {
  358. return "ALTER TABLE " . $this->db->quoteTableName($table)
  359. . " RENAME COLUMN " . $this->db->quoteColumnName($oldName)
  360. . " TO " . $this->db->quoteColumnName($newName);
  361. }
  362. /**
  363. * Builds a SQL statement for changing the definition of a column.
  364. * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
  365. * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
  366. * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
  367. * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
  368. * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
  369. * will become 'varchar(255) not null'.
  370. * @return string the SQL statement for changing the definition of a column.
  371. */
  372. public function alterColumn($table, $column, $type)
  373. {
  374. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
  375. . $this->db->quoteColumnName($column) . ' '
  376. . $this->db->quoteColumnName($column) . ' '
  377. . $this->getColumnType($type);
  378. }
  379. /**
  380. * Builds a SQL statement for adding a foreign key constraint to an existing table.
  381. * The method will properly quote the table and column names.
  382. * @param string $name the name of the foreign key constraint.
  383. * @param string $table the table that the foreign key constraint will be added to.
  384. * @param string|array $columns the name of the column to that the constraint will be added on.
  385. * If there are multiple columns, separate them with commas or use an array to represent them.
  386. * @param string $refTable the table that the foreign key references to.
  387. * @param string|array $refColumns the name of the column that the foreign key references to.
  388. * If there are multiple columns, separate them with commas or use an array to represent them.
  389. * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  390. * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  391. * @return string the SQL statement for adding a foreign key constraint to an existing table.
  392. */
  393. public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
  394. {
  395. $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
  396. . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
  397. . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
  398. . ' REFERENCES ' . $this->db->quoteTableName($refTable)
  399. . ' (' . $this->buildColumns($refColumns) . ')';
  400. if ($delete !== null) {
  401. $sql .= ' ON DELETE ' . $delete;
  402. }
  403. if ($update !== null) {
  404. $sql .= ' ON UPDATE ' . $update;
  405. }
  406. return $sql;
  407. }
  408. /**
  409. * Builds a SQL statement for dropping a foreign key constraint.
  410. * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
  411. * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
  412. * @return string the SQL statement for dropping a foreign key constraint.
  413. */
  414. public function dropForeignKey($name, $table)
  415. {
  416. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  417. . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
  418. }
  419. /**
  420. * Builds a SQL statement for creating a new index.
  421. * @param string $name the name of the index. The name will be properly quoted by the method.
  422. * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
  423. * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
  424. * separate them with commas or use an array to represent them. Each column name will be properly quoted
  425. * by the method, unless a parenthesis is found in the name.
  426. * @param boolean $unique whether to add UNIQUE constraint on the created index.
  427. * @return string the SQL statement for creating a new index.
  428. */
  429. public function createIndex($name, $table, $columns, $unique = false)
  430. {
  431. return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
  432. . $this->db->quoteTableName($name) . ' ON '
  433. . $this->db->quoteTableName($table)
  434. . ' (' . $this->buildColumns($columns) . ')';
  435. }
  436. /**
  437. * Builds a SQL statement for dropping an index.
  438. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
  439. * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
  440. * @return string the SQL statement for dropping an index.
  441. */
  442. public function dropIndex($name, $table)
  443. {
  444. return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
  445. }
  446. /**
  447. * Creates a SQL statement for resetting the sequence value of a table's primary key.
  448. * The sequence will be reset such that the primary key of the next new row inserted
  449. * will have the specified value or 1.
  450. * @param string $table the name of the table whose primary key sequence will be reset
  451. * @param array|string $value the value for the primary key of the next new row inserted. If this is not set,
  452. * the next new row's primary key will have a value 1.
  453. * @return string the SQL statement for resetting sequence
  454. * @throws NotSupportedException if this is not supported by the underlying DBMS
  455. */
  456. public function resetSequence($table, $value = null)
  457. {
  458. throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
  459. }
  460. /**
  461. * Builds a SQL statement for enabling or disabling integrity check.
  462. * @param boolean $check whether to turn on or off the integrity check.
  463. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
  464. * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
  465. * @return string the SQL statement for checking integrity
  466. * @throws NotSupportedException if this is not supported by the underlying DBMS
  467. */
  468. public function checkIntegrity($check = true, $schema = '', $table = '')
  469. {
  470. throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.');
  471. }
  472. /**
  473. * Converts an abstract column type into a physical column type.
  474. * The conversion is done using the type map specified in [[typeMap]].
  475. * The following abstract column types are supported (using MySQL as an example to explain the corresponding
  476. * physical types):
  477. *
  478. * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
  479. * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY"
  480. * - `string`: string type, will be converted into "varchar(255)"
  481. * - `text`: a long string type, will be converted into "text"
  482. * - `smallint`: a small integer type, will be converted into "smallint(6)"
  483. * - `integer`: integer type, will be converted into "int(11)"
  484. * - `bigint`: a big integer type, will be converted into "bigint(20)"
  485. * - `boolean`: boolean type, will be converted into "tinyint(1)"
  486. * - `float``: float number type, will be converted into "float"
  487. * - `decimal`: decimal number type, will be converted into "decimal"
  488. * - `datetime`: datetime type, will be converted into "datetime"
  489. * - `timestamp`: timestamp type, will be converted into "timestamp"
  490. * - `time`: time type, will be converted into "time"
  491. * - `date`: date type, will be converted into "date"
  492. * - `money`: money type, will be converted into "decimal(19,4)"
  493. * - `binary`: binary data type, will be converted into "blob"
  494. *
  495. * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only
  496. * the first part will be converted, and the rest of the parts will be appended to the converted result.
  497. * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
  498. *
  499. * For some of the abstract types you can also specify a length or precision constraint
  500. * by prepending it in round brackets directly to the type.
  501. * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
  502. * If the underlying DBMS does not support these kind of constraints for a type it will
  503. * be ignored.
  504. *
  505. * If a type cannot be found in [[typeMap]], it will be returned without any change.
  506. * @param string $type abstract column type
  507. * @return string physical column type.
  508. */
  509. public function getColumnType($type)
  510. {
  511. if (isset($this->typeMap[$type])) {
  512. return $this->typeMap[$type];
  513. } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
  514. if (isset($this->typeMap[$matches[1]])) {
  515. return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
  516. }
  517. } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
  518. if (isset($this->typeMap[$matches[1]])) {
  519. return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
  520. }
  521. }
  522. return $type;
  523. }
  524. /**
  525. * @param array $columns
  526. * @param boolean $distinct
  527. * @param string $selectOption
  528. * @return string the SELECT clause built from [[query]].
  529. */
  530. public function buildSelect($columns, $distinct = false, $selectOption = null)
  531. {
  532. $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
  533. if ($selectOption !== null) {
  534. $select .= ' ' . $selectOption;
  535. }
  536. if (empty($columns)) {
  537. return $select . ' *';
  538. }
  539. foreach ($columns as $i => $column) {
  540. if (is_object($column)) {
  541. $columns[$i] = (string)$column;
  542. } elseif (strpos($column, '(') === false) {
  543. if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
  544. $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
  545. } else {
  546. $columns[$i] = $this->db->quoteColumnName($column);
  547. }
  548. }
  549. }
  550. if (is_array($columns)) {
  551. $columns = implode(', ', $columns);
  552. }
  553. return $select . ' ' . $columns;
  554. }
  555. /**
  556. * @param array $tables
  557. * @return string the FROM clause built from [[query]].
  558. */
  559. public function buildFrom($tables)
  560. {
  561. if (empty($tables)) {
  562. return '';
  563. }
  564. foreach ($tables as $i => $table) {
  565. if (strpos($table, '(') === false) {
  566. if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
  567. $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
  568. } else {
  569. $tables[$i] = $this->db->quoteTableName($table);
  570. }
  571. }
  572. }
  573. if (is_array($tables)) {
  574. $tables = implode(', ', $tables);
  575. }
  576. return 'FROM ' . $tables;
  577. }
  578. /**
  579. * @param string|array $joins
  580. * @param array $params the binding parameters to be populated
  581. * @return string the JOIN clause built from [[query]].
  582. * @throws Exception if the $joins parameter is not in proper format
  583. */
  584. public function buildJoin($joins, &$params)
  585. {
  586. if (empty($joins)) {
  587. return '';
  588. }
  589. foreach ($joins as $i => $join) {
  590. if (is_object($join)) {
  591. $joins[$i] = (string)$join;
  592. } elseif (is_array($join) && isset($join[0], $join[1])) {
  593. // 0:join type, 1:table name, 2:on-condition
  594. $table = $join[1];
  595. if (strpos($table, '(') === false) {
  596. if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
  597. $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
  598. } else {
  599. $table = $this->db->quoteTableName($table);
  600. }
  601. }
  602. $joins[$i] = $join[0] . ' ' . $table;
  603. if (isset($join[2])) {
  604. $condition = $this->buildCondition($join[2], $params);
  605. if ($condition !== '') {
  606. $joins[$i] .= ' ON ' . $condition;
  607. }
  608. }
  609. } else {
  610. throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
  611. }
  612. }
  613. return implode($this->separator, $joins);
  614. }
  615. /**
  616. * @param string|array $condition
  617. * @param array $params the binding parameters to be populated
  618. * @return string the WHERE clause built from [[query]].
  619. */
  620. public function buildWhere($condition, &$params)
  621. {
  622. $where = $this->buildCondition($condition, $params);
  623. return $where === '' ? '' : 'WHERE ' . $where;
  624. }
  625. /**
  626. * @param array $columns
  627. * @return string the GROUP BY clause
  628. */
  629. public function buildGroupBy($columns)
  630. {
  631. return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns);
  632. }
  633. /**
  634. * @param string|array $condition
  635. * @param array $params the binding parameters to be populated
  636. * @return string the HAVING clause built from [[query]].
  637. */
  638. public function buildHaving($condition, &$params)
  639. {
  640. $having = $this->buildCondition($condition, $params);
  641. return $having === '' ? '' : 'HAVING ' . $having;
  642. }
  643. /**
  644. * @param array $columns
  645. * @return string the ORDER BY clause built from [[query]].
  646. */
  647. public function buildOrderBy($columns)
  648. {
  649. if (empty($columns)) {
  650. return '';
  651. }
  652. $orders = [];
  653. foreach ($columns as $name => $direction) {
  654. if (is_object($direction)) {
  655. $orders[] = (string)$direction;
  656. } else {
  657. $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
  658. }
  659. }
  660. return 'ORDER BY ' . implode(', ', $orders);
  661. }
  662. /**
  663. * @param integer $limit
  664. * @param integer $offset
  665. * @return string the LIMIT and OFFSET clauses built from [[query]].
  666. */
  667. public function buildLimit($limit, $offset)
  668. {
  669. $sql = '';
  670. if ($limit !== null && $limit >= 0) {
  671. $sql = 'LIMIT ' . (int)$limit;
  672. }
  673. if ($offset > 0) {
  674. $sql .= ' OFFSET ' . (int)$offset;
  675. }
  676. return ltrim($sql);
  677. }
  678. /**
  679. * @param array $unions
  680. * @param array $params the binding parameters to be populated
  681. * @return string the UNION clause built from [[query]].
  682. */
  683. public function buildUnion($unions, &$params)
  684. {
  685. if (empty($unions)) {
  686. return '';
  687. }
  688. foreach ($unions as $i => $union) {
  689. if ($union instanceof Query) {
  690. // save the original parameters so that we can restore them later to prevent from modifying the query object
  691. $originalParams = $union->params;
  692. $union->addParams($params);
  693. list ($unions[$i], $params) = $this->build($union);
  694. $union->params = $originalParams;
  695. }
  696. }
  697. return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)";
  698. }
  699. /**
  700. * Processes columns and properly quote them if necessary.
  701. * It will join all columns into a string with comma as separators.
  702. * @param string|array $columns the columns to be processed
  703. * @return string the processing result
  704. */
  705. public function buildColumns($columns)
  706. {
  707. if (!is_array($columns)) {
  708. if (strpos($columns, '(') !== false) {
  709. return $columns;
  710. } else {
  711. $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
  712. }
  713. }
  714. foreach ($columns as $i => $column) {
  715. if (is_object($column)) {
  716. $columns[$i] = (string)$column;
  717. } elseif (strpos($column, '(') === false) {
  718. $columns[$i] = $this->db->quoteColumnName($column);
  719. }
  720. }
  721. return is_array($columns) ? implode(', ', $columns) : $columns;
  722. }
  723. /**
  724. * Parses the condition specification and generates the corresponding SQL expression.
  725. * @param string|array $condition the condition specification. Please refer to [[Query::where()]]
  726. * on how to specify a condition.
  727. * @param array $params the binding parameters to be populated
  728. * @return string the generated SQL expression
  729. * @throws InvalidParamException if the condition is in bad format
  730. */
  731. public function buildCondition($condition, &$params)
  732. {
  733. static $builders = [
  734. 'NOT' => 'buildNotCondition',
  735. 'AND' => 'buildAndCondition',
  736. 'OR' => 'buildAndCondition',
  737. 'BETWEEN' => 'buildBetweenCondition',
  738. 'NOT BETWEEN' => 'buildBetweenCondition',
  739. 'IN' => 'buildInCondition',
  740. 'NOT IN' => 'buildInCondition',
  741. 'LIKE' => 'buildLikeCondition',
  742. 'NOT LIKE' => 'buildLikeCondition',
  743. 'OR LIKE' => 'buildLikeCondition',
  744. 'OR NOT LIKE' => 'buildLikeCondition',
  745. 'EXISTS' => 'buildExistsCondition',
  746. 'NOT EXISTS' => 'buildExistsCondition',
  747. ];
  748. if (!is_array($condition)) {
  749. return (string)$condition;
  750. } elseif (empty($condition)) {
  751. return '';
  752. }
  753. if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
  754. $operator = strtoupper($condition[0]);
  755. if (isset($builders[$operator])) {
  756. $method = $builders[$operator];
  757. array_shift($condition);
  758. return $this->$method($operator, $condition, $params);
  759. } else {
  760. throw new InvalidParamException('Found unknown operator in query: ' . $operator);
  761. }
  762. } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
  763. return $this->buildHashCondition($condition, $params);
  764. }
  765. }
  766. /**
  767. * Creates a condition based on column-value pairs.
  768. * @param array $condition the condition specification.
  769. * @param array $params the binding parameters to be populated
  770. * @return string the generated SQL expression
  771. */
  772. public function buildHashCondition($condition, &$params)
  773. {
  774. $parts = [];
  775. foreach ($condition as $column => $value) {
  776. if (is_array($value)) { // IN condition
  777. $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
  778. } else {
  779. if (strpos($column, '(') === false) {
  780. $column = $this->db->quoteColumnName($column);
  781. }
  782. if ($value === null) {
  783. $parts[] = "$column IS NULL";
  784. } elseif ($value instanceof Expression) {
  785. $parts[] = "$column=" . $value->expression;
  786. foreach ($value->params as $n => $v) {
  787. $params[$n] = $v;
  788. }
  789. } else {
  790. $phName = self::PARAM_PREFIX . count($params);
  791. $parts[] = "$column=$phName";
  792. $params[$phName] = $value;
  793. }
  794. }
  795. }
  796. return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
  797. }
  798. /**
  799. * Connects two or more SQL expressions with the `AND` or `OR` operator.
  800. * @param string $operator the operator to use for connecting the given operands
  801. * @param array $operands the SQL expressions to connect.
  802. * @param array $params the binding parameters to be populated
  803. * @return string the generated SQL expression
  804. */
  805. public function buildAndCondition($operator, $operands, &$params)
  806. {
  807. $parts = [];
  808. foreach ($operands as $operand) {
  809. if (is_array($operand)) {
  810. $operand = $this->buildCondition($operand, $params);
  811. }
  812. if ($operand !== '') {
  813. $parts[] = $operand;
  814. }
  815. }
  816. if (!empty($parts)) {
  817. return '(' . implode(") $operator (", $parts) . ')';
  818. } else {
  819. return '';
  820. }
  821. }
  822. /**
  823. * Inverts an SQL expressions with `NOT` operator.
  824. * @param string $operator the operator to use for connecting the given operands
  825. * @param array $operands the SQL expressions to connect.
  826. * @param array $params the binding parameters to be populated
  827. * @return string the generated SQL expression
  828. * @throws InvalidParamException if wrong number of operands have been given.
  829. */
  830. public function buildNotCondition($operator, $operands, &$params)
  831. {
  832. if (count($operands) != 1) {
  833. throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
  834. }
  835. $operand = reset($operands);
  836. if (is_array($operand)) {
  837. $operand = $this->buildCondition($operand, $params);
  838. }
  839. if ($operand === '') {
  840. return '';
  841. }
  842. return "$operator ($operand)";
  843. }
  844. /**
  845. * Creates an SQL expressions with the `BETWEEN` operator.
  846. * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
  847. * @param array $operands the first operand is the column name. The second and third operands
  848. * describe the interval that column value should be in.
  849. * @param array $params the binding parameters to be populated
  850. * @return string the generated SQL expression
  851. * @throws InvalidParamException if wrong number of operands have been given.
  852. */
  853. public function buildBetweenCondition($operator, $operands, &$params)
  854. {
  855. if (!isset($operands[0], $operands[1], $operands[2])) {
  856. throw new InvalidParamException("Operator '$operator' requires three operands.");
  857. }
  858. list($column, $value1, $value2) = $operands;
  859. if (strpos($column, '(') === false) {
  860. $column = $this->db->quoteColumnName($column);
  861. }
  862. $phName1 = self::PARAM_PREFIX . count($params);
  863. $params[$phName1] = $value1;
  864. $phName2 = self::PARAM_PREFIX . count($params);
  865. $params[$phName2] = $value2;
  866. return "$column $operator $phName1 AND $phName2";
  867. }
  868. /**
  869. * Creates an SQL expressions with the `IN` operator.
  870. * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
  871. * @param array $operands the first operand is the column name. If it is an array
  872. * a composite IN condition will be generated.
  873. * The second operand is an array of values that column value should be among.
  874. * If it is an empty array the generated expression will be a `false` value if
  875. * operator is `IN` and empty if operator is `NOT IN`.
  876. * @param array $params the binding parameters to be populated
  877. * @return string the generated SQL expression
  878. * @throws Exception if wrong number of operands have been given.
  879. */
  880. public function buildInCondition($operator, $operands, &$params)
  881. {
  882. if (!isset($operands[0], $operands[1])) {
  883. throw new Exception("Operator '$operator' requires two operands.");
  884. }
  885. list($column, $values) = $operands;
  886. $values = (array)$values;
  887. if (empty($values) || $column === []) {
  888. return $operator === 'IN' ? '0=1' : '';
  889. }
  890. if (count($column) > 1) {
  891. return $this->buildCompositeInCondition($operator, $column, $values, $params);
  892. } elseif (is_array($column)) {
  893. $column = reset($column);
  894. }
  895. foreach ($values as $i => $value) {
  896. if (is_array($value)) {
  897. $value = isset($value[$column]) ? $value[$column] : null;
  898. }
  899. if ($value === null) {
  900. $values[$i] = 'NULL';
  901. } elseif ($value instanceof Expression) {
  902. $values[$i] = $value->expression;
  903. foreach ($value->params as $n => $v) {
  904. $params[$n] = $v;
  905. }
  906. } else {
  907. $phName = self::PARAM_PREFIX . count($params);
  908. $params[$phName] = $value;
  909. $values[$i] = $phName;
  910. }
  911. }
  912. if (strpos($column, '(') === false) {
  913. $column = $this->db->quoteColumnName($column);
  914. }
  915. if (count($values) > 1) {
  916. return "$column $operator (" . implode(', ', $values) . ')';
  917. } else {
  918. $operator = $operator === 'IN' ? '=' : '<>';
  919. return "$column$operator{$values[0]}";
  920. }
  921. }
  922. protected function buildCompositeInCondition($operator, $columns, $values, &$params)
  923. {
  924. $vss = [];
  925. foreach ($values as $value) {
  926. $vs = [];
  927. foreach ($columns as $column) {
  928. if (isset($value[$column])) {
  929. $phName = self::PARAM_PREFIX . count($params);
  930. $params[$phName] = $value[$column];
  931. $vs[] = $phName;
  932. } else {
  933. $vs[] = 'NULL';
  934. }
  935. }
  936. $vss[] = '(' . implode(', ', $vs) . ')';
  937. }
  938. foreach ($columns as $i => $column) {
  939. if (strpos($column, '(') === false) {
  940. $columns[$i] = $this->db->quoteColumnName($column);
  941. }
  942. }
  943. return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
  944. }
  945. /**
  946. * Creates an SQL expressions with the `LIKE` operator.
  947. * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
  948. * @param array $operands an array of two or three operands
  949. *
  950. * - The first operand is the column name.
  951. * - The second operand is a single value or an array of values that column value
  952. * should be compared with. If it is an empty array the generated expression will
  953. * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
  954. * is `NOT LIKE` or `OR NOT LIKE`.
  955. * - An optional third operand can also be provided to specify how to escape special characters
  956. * in the value(s). The operand should be an array of mappings from the special characters to their
  957. * escaped counterparts. If this operand is not provided, a default escape mapping will be used.
  958. * You may use `false` or an empty array to indicate the values are already escaped and no escape
  959. * should be applied. Note that when using an escape mapping (or the third operand is not provided),
  960. * the values will be automatically enclosed within a pair of percentage characters.
  961. * @param array $params the binding parameters to be populated
  962. * @return string the generated SQL expression
  963. * @throws InvalidParamException if wrong number of operands have been given.
  964. */
  965. public function buildLikeCondition($operator, $operands, &$params)
  966. {
  967. if (!isset($operands[0], $operands[1])) {
  968. throw new InvalidParamException("Operator '$operator' requires two operands.");
  969. }
  970. $escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\'];
  971. unset($operands[2]);
  972. list($column, $values) = $operands;
  973. $values = (array)$values;
  974. if (empty($values)) {
  975. return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : '';
  976. }
  977. if ($operator === 'LIKE' || $operator === 'NOT LIKE') {
  978. $andor = ' AND ';
  979. } else {
  980. $andor = ' OR ';
  981. $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
  982. }
  983. if (strpos($column, '(') === false) {
  984. $column = $this->db->quoteColumnName($column);
  985. }
  986. $parts = [];
  987. foreach ($values as $value) {
  988. $phName = self::PARAM_PREFIX . count($params);
  989. $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
  990. $parts[] = "$column $operator $phName";
  991. }
  992. return implode($andor, $parts);
  993. }
  994. /**
  995. * Creates an SQL expressions with the `EXISTS` operator.
  996. * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
  997. * @param array $operands contains only one element which is a [[Query]] object representing the sub-query.
  998. * @param array $params the binding parameters to be populated
  999. * @return string the generated SQL expression
  1000. */
  1001. public function buildExistsCondition($operator, $operands, &$params)
  1002. {
  1003. $subQuery = $operands[0];
  1004. list($subQuerySql, $subQueryParams) = $this->build($subQuery);
  1005. if (!empty($subQueryParams)) {
  1006. foreach ($subQueryParams as $name => $value) {
  1007. $params[$name] = $value;
  1008. }
  1009. }
  1010. return "$operator ($subQuerySql)";
  1011. }
  1012. }