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.

1425 line
56KB

  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. use yii\helpers\ArrayHelper;
  11. /**
  12. * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
  13. *
  14. * SQL statements are created from [[Query]] objects using the [[build()]]-method.
  15. *
  16. * QueryBuilder is also used by [[Command]] to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
  17. *
  18. * @author Qiang Xue <qiang.xue@gmail.com>
  19. * @since 2.0
  20. */
  21. class QueryBuilder extends \yii\base\Object
  22. {
  23. /**
  24. * The prefix for automatically generated query binding parameters.
  25. */
  26. const PARAM_PREFIX = ':qp';
  27. /**
  28. * @var Connection the database connection.
  29. */
  30. public $db;
  31. /**
  32. * @var string the separator between different fragments of a SQL statement.
  33. * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
  34. */
  35. public $separator = ' ';
  36. /**
  37. * @var array the abstract column types mapped to physical column types.
  38. * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
  39. * Child classes should override this property to declare supported type mappings.
  40. */
  41. public $typeMap = [];
  42. /**
  43. * @var array map of query condition to builder methods.
  44. * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
  45. */
  46. protected $conditionBuilders = [
  47. 'NOT' => 'buildNotCondition',
  48. 'AND' => 'buildAndCondition',
  49. 'OR' => 'buildAndCondition',
  50. 'BETWEEN' => 'buildBetweenCondition',
  51. 'NOT BETWEEN' => 'buildBetweenCondition',
  52. 'IN' => 'buildInCondition',
  53. 'NOT IN' => 'buildInCondition',
  54. 'LIKE' => 'buildLikeCondition',
  55. 'NOT LIKE' => 'buildLikeCondition',
  56. 'OR LIKE' => 'buildLikeCondition',
  57. 'OR NOT LIKE' => 'buildLikeCondition',
  58. 'EXISTS' => 'buildExistsCondition',
  59. 'NOT EXISTS' => 'buildExistsCondition',
  60. ];
  61. /**
  62. * Constructor.
  63. * @param Connection $connection the database connection.
  64. * @param array $config name-value pairs that will be used to initialize the object properties
  65. */
  66. public function __construct($connection, $config = [])
  67. {
  68. $this->db = $connection;
  69. parent::__construct($config);
  70. }
  71. /**
  72. * Generates a SELECT SQL statement from a [[Query]] object.
  73. * @param Query $query the [[Query]] object from which the SQL statement will be generated.
  74. * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
  75. * be included in the result with the additional parameters generated during the query building process.
  76. * @return array the generated SQL statement (the first array element) and the corresponding
  77. * parameters to be bound to the SQL statement (the second array element). The parameters returned
  78. * include those provided in `$params`.
  79. */
  80. public function build($query, $params = [])
  81. {
  82. $query = $query->prepare($this);
  83. $params = empty($params) ? $query->params : array_merge($params, $query->params);
  84. $clauses = [
  85. $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
  86. $this->buildFrom($query->from, $params),
  87. $this->buildJoin($query->join, $params),
  88. $this->buildWhere($query->where, $params),
  89. $this->buildGroupBy($query->groupBy),
  90. $this->buildHaving($query->having, $params),
  91. ];
  92. $sql = implode($this->separator, array_filter($clauses));
  93. $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
  94. if (!empty($query->orderBy)) {
  95. foreach ($query->orderBy as $expression) {
  96. if ($expression instanceof Expression) {
  97. $params = array_merge($params, $expression->params);
  98. }
  99. }
  100. }
  101. if (!empty($query->groupBy)) {
  102. foreach ($query->groupBy as $expression) {
  103. if ($expression instanceof Expression) {
  104. $params = array_merge($params, $expression->params);
  105. }
  106. }
  107. }
  108. $union = $this->buildUnion($query->union, $params);
  109. if ($union !== '') {
  110. $sql = "($sql){$this->separator}$union";
  111. }
  112. return [$sql, $params];
  113. }
  114. /**
  115. * Creates an INSERT SQL statement.
  116. * For example,
  117. *
  118. * ```php
  119. * $sql = $queryBuilder->insert('user', [
  120. * 'name' => 'Sam',
  121. * 'age' => 30,
  122. * ], $params);
  123. * ```
  124. *
  125. * The method will properly escape the table and column names.
  126. *
  127. * @param string $table the table that new rows will be inserted into.
  128. * @param array $columns the column data (name => value) to be inserted into the table.
  129. * @param array $params the binding parameters that will be generated by this method.
  130. * They should be bound to the DB command later.
  131. * @return string the INSERT SQL
  132. */
  133. public function insert($table, $columns, &$params)
  134. {
  135. $schema = $this->db->getSchema();
  136. if (($tableSchema = $schema->getTableSchema($table)) !== null) {
  137. $columnSchemas = $tableSchema->columns;
  138. } else {
  139. $columnSchemas = [];
  140. }
  141. $names = [];
  142. $placeholders = [];
  143. foreach ($columns as $name => $value) {
  144. $names[] = $schema->quoteColumnName($name);
  145. if ($value instanceof Expression) {
  146. $placeholders[] = $value->expression;
  147. foreach ($value->params as $n => $v) {
  148. $params[$n] = $v;
  149. }
  150. } else {
  151. $phName = self::PARAM_PREFIX . count($params);
  152. $placeholders[] = $phName;
  153. $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
  154. }
  155. }
  156. return 'INSERT INTO ' . $schema->quoteTableName($table)
  157. . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
  158. . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' DEFAULT VALUES');
  159. }
  160. /**
  161. * Generates a batch INSERT SQL statement.
  162. * For example,
  163. *
  164. * ```php
  165. * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
  166. * ['Tom', 30],
  167. * ['Jane', 20],
  168. * ['Linda', 25],
  169. * ]);
  170. * ```
  171. *
  172. * Note that the values in each row must match the corresponding column names.
  173. *
  174. * The method will properly escape the column names, and quote the values to be inserted.
  175. *
  176. * @param string $table the table that new rows will be inserted into.
  177. * @param array $columns the column names
  178. * @param array $rows the rows to be batch inserted into the table
  179. * @return string the batch INSERT SQL statement
  180. */
  181. public function batchInsert($table, $columns, $rows)
  182. {
  183. if (empty($rows)) {
  184. return '';
  185. }
  186. $schema = $this->db->getSchema();
  187. if (($tableSchema = $schema->getTableSchema($table)) !== null) {
  188. $columnSchemas = $tableSchema->columns;
  189. } else {
  190. $columnSchemas = [];
  191. }
  192. $values = [];
  193. foreach ($rows as $row) {
  194. $vs = [];
  195. foreach ($row as $i => $value) {
  196. if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
  197. $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
  198. }
  199. if (is_string($value)) {
  200. $value = $schema->quoteValue($value);
  201. } elseif ($value === false) {
  202. $value = 0;
  203. } elseif ($value === null) {
  204. $value = 'NULL';
  205. }
  206. $vs[] = $value;
  207. }
  208. $values[] = '(' . implode(', ', $vs) . ')';
  209. }
  210. foreach ($columns as $i => $name) {
  211. $columns[$i] = $schema->quoteColumnName($name);
  212. }
  213. return 'INSERT INTO ' . $schema->quoteTableName($table)
  214. . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
  215. }
  216. /**
  217. * Creates an UPDATE SQL statement.
  218. * For example,
  219. *
  220. * ```php
  221. * $params = [];
  222. * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
  223. * ```
  224. *
  225. * The method will properly escape the table and column names.
  226. *
  227. * @param string $table the table to be updated.
  228. * @param array $columns the column data (name => value) to be updated.
  229. * @param array|string $condition the condition that will be put in the WHERE part. Please
  230. * refer to [[Query::where()]] on how to specify condition.
  231. * @param array $params the binding parameters that will be modified by this method
  232. * so that they can be bound to the DB command later.
  233. * @return string the UPDATE SQL
  234. */
  235. public function update($table, $columns, $condition, &$params)
  236. {
  237. if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
  238. $columnSchemas = $tableSchema->columns;
  239. } else {
  240. $columnSchemas = [];
  241. }
  242. $lines = [];
  243. foreach ($columns as $name => $value) {
  244. if ($value instanceof Expression) {
  245. $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
  246. foreach ($value->params as $n => $v) {
  247. $params[$n] = $v;
  248. }
  249. } else {
  250. $phName = self::PARAM_PREFIX . count($params);
  251. $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
  252. $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
  253. }
  254. }
  255. $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
  256. $where = $this->buildWhere($condition, $params);
  257. return $where === '' ? $sql : $sql . ' ' . $where;
  258. }
  259. /**
  260. * Creates a DELETE SQL statement.
  261. * For example,
  262. *
  263. * ```php
  264. * $sql = $queryBuilder->delete('user', 'status = 0');
  265. * ```
  266. *
  267. * The method will properly escape the table and column names.
  268. *
  269. * @param string $table the table where the data will be deleted from.
  270. * @param array|string $condition the condition that will be put in the WHERE part. Please
  271. * refer to [[Query::where()]] on how to specify condition.
  272. * @param array $params the binding parameters that will be modified by this method
  273. * so that they can be bound to the DB command later.
  274. * @return string the DELETE SQL
  275. */
  276. public function delete($table, $condition, &$params)
  277. {
  278. $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
  279. $where = $this->buildWhere($condition, $params);
  280. return $where === '' ? $sql : $sql . ' ' . $where;
  281. }
  282. /**
  283. * Builds a SQL statement for creating a new DB table.
  284. *
  285. * The columns in the new table should be specified as name-definition pairs (e.g. 'name' => 'string'),
  286. * where name stands for a column name which will be properly quoted by the method, and definition
  287. * stands for the column type which can contain an abstract DB type.
  288. * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
  289. *
  290. * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
  291. * inserted into the generated SQL.
  292. *
  293. * For example,
  294. *
  295. * ```php
  296. * $sql = $queryBuilder->createTable('user', [
  297. * 'id' => 'pk',
  298. * 'name' => 'string',
  299. * 'age' => 'integer',
  300. * ]);
  301. * ```
  302. *
  303. * @param string $table the name of the table to be created. The name will be properly quoted by the method.
  304. * @param array $columns the columns (name => definition) in the new table.
  305. * @param string $options additional SQL fragment that will be appended to the generated SQL.
  306. * @return string the SQL statement for creating a new DB table.
  307. */
  308. public function createTable($table, $columns, $options = null)
  309. {
  310. $cols = [];
  311. foreach ($columns as $name => $type) {
  312. if (is_string($name)) {
  313. $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
  314. } else {
  315. $cols[] = "\t" . $type;
  316. }
  317. }
  318. $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
  319. return $options === null ? $sql : $sql . ' ' . $options;
  320. }
  321. /**
  322. * Builds a SQL statement for renaming a DB table.
  323. * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
  324. * @param string $newName the new table name. The name will be properly quoted by the method.
  325. * @return string the SQL statement for renaming a DB table.
  326. */
  327. public function renameTable($oldName, $newName)
  328. {
  329. return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
  330. }
  331. /**
  332. * Builds a SQL statement for dropping a DB table.
  333. * @param string $table the table to be dropped. The name will be properly quoted by the method.
  334. * @return string the SQL statement for dropping a DB table.
  335. */
  336. public function dropTable($table)
  337. {
  338. return 'DROP TABLE ' . $this->db->quoteTableName($table);
  339. }
  340. /**
  341. * Builds a SQL statement for adding a primary key constraint to an existing table.
  342. * @param string $name the name of the primary key constraint.
  343. * @param string $table the table that the primary key constraint will be added to.
  344. * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
  345. * @return string the SQL statement for adding a primary key constraint to an existing table.
  346. */
  347. public function addPrimaryKey($name, $table, $columns)
  348. {
  349. if (is_string($columns)) {
  350. $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
  351. }
  352. foreach ($columns as $i => $col) {
  353. $columns[$i] = $this->db->quoteColumnName($col);
  354. }
  355. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
  356. . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
  357. . implode(', ', $columns). ' )';
  358. }
  359. /**
  360. * Builds a SQL statement for removing a primary key constraint to an existing table.
  361. * @param string $name the name of the primary key constraint to be removed.
  362. * @param string $table the table that the primary key constraint will be removed from.
  363. * @return string the SQL statement for removing a primary key constraint from an existing table.
  364. */
  365. public function dropPrimaryKey($name, $table)
  366. {
  367. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  368. . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
  369. }
  370. /**
  371. * Builds a SQL statement for truncating a DB table.
  372. * @param string $table the table to be truncated. The name will be properly quoted by the method.
  373. * @return string the SQL statement for truncating a DB table.
  374. */
  375. public function truncateTable($table)
  376. {
  377. return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
  378. }
  379. /**
  380. * Builds a SQL statement for adding a new DB column.
  381. * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
  382. * @param string $column the name of the new column. The name will be properly quoted by the method.
  383. * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
  384. * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
  385. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
  386. * @return string the SQL statement for adding a new column.
  387. */
  388. public function addColumn($table, $column, $type)
  389. {
  390. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  391. . ' ADD ' . $this->db->quoteColumnName($column) . ' '
  392. . $this->getColumnType($type);
  393. }
  394. /**
  395. * Builds a SQL statement for dropping a DB column.
  396. * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
  397. * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
  398. * @return string the SQL statement for dropping a DB column.
  399. */
  400. public function dropColumn($table, $column)
  401. {
  402. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  403. . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
  404. }
  405. /**
  406. * Builds a SQL statement for renaming a column.
  407. * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
  408. * @param string $oldName the old name of the column. The name will be properly quoted by the method.
  409. * @param string $newName the new name of the column. The name will be properly quoted by the method.
  410. * @return string the SQL statement for renaming a DB column.
  411. */
  412. public function renameColumn($table, $oldName, $newName)
  413. {
  414. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  415. . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
  416. . ' TO ' . $this->db->quoteColumnName($newName);
  417. }
  418. /**
  419. * Builds a SQL statement for changing the definition of a column.
  420. * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
  421. * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
  422. * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
  423. * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
  424. * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
  425. * will become 'varchar(255) not null'.
  426. * @return string the SQL statement for changing the definition of a column.
  427. */
  428. public function alterColumn($table, $column, $type)
  429. {
  430. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
  431. . $this->db->quoteColumnName($column) . ' '
  432. . $this->db->quoteColumnName($column) . ' '
  433. . $this->getColumnType($type);
  434. }
  435. /**
  436. * Builds a SQL statement for adding a foreign key constraint to an existing table.
  437. * The method will properly quote the table and column names.
  438. * @param string $name the name of the foreign key constraint.
  439. * @param string $table the table that the foreign key constraint will be added to.
  440. * @param string|array $columns the name of the column to that the constraint will be added on.
  441. * If there are multiple columns, separate them with commas or use an array to represent them.
  442. * @param string $refTable the table that the foreign key references to.
  443. * @param string|array $refColumns the name of the column that the foreign key references to.
  444. * If there are multiple columns, separate them with commas or use an array to represent them.
  445. * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  446. * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
  447. * @return string the SQL statement for adding a foreign key constraint to an existing table.
  448. */
  449. public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
  450. {
  451. $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
  452. . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
  453. . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
  454. . ' REFERENCES ' . $this->db->quoteTableName($refTable)
  455. . ' (' . $this->buildColumns($refColumns) . ')';
  456. if ($delete !== null) {
  457. $sql .= ' ON DELETE ' . $delete;
  458. }
  459. if ($update !== null) {
  460. $sql .= ' ON UPDATE ' . $update;
  461. }
  462. return $sql;
  463. }
  464. /**
  465. * Builds a SQL statement for dropping a foreign key constraint.
  466. * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
  467. * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
  468. * @return string the SQL statement for dropping a foreign key constraint.
  469. */
  470. public function dropForeignKey($name, $table)
  471. {
  472. return 'ALTER TABLE ' . $this->db->quoteTableName($table)
  473. . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
  474. }
  475. /**
  476. * Builds a SQL statement for creating a new index.
  477. * @param string $name the name of the index. The name will be properly quoted by the method.
  478. * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
  479. * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
  480. * separate them with commas or use an array to represent them. Each column name will be properly quoted
  481. * by the method, unless a parenthesis is found in the name.
  482. * @param boolean $unique whether to add UNIQUE constraint on the created index.
  483. * @return string the SQL statement for creating a new index.
  484. */
  485. public function createIndex($name, $table, $columns, $unique = false)
  486. {
  487. return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
  488. . $this->db->quoteTableName($name) . ' ON '
  489. . $this->db->quoteTableName($table)
  490. . ' (' . $this->buildColumns($columns) . ')';
  491. }
  492. /**
  493. * Builds a SQL statement for dropping an index.
  494. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
  495. * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
  496. * @return string the SQL statement for dropping an index.
  497. */
  498. public function dropIndex($name, $table)
  499. {
  500. return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
  501. }
  502. /**
  503. * Creates a SQL statement for resetting the sequence value of a table's primary key.
  504. * The sequence will be reset such that the primary key of the next new row inserted
  505. * will have the specified value or 1.
  506. * @param string $table the name of the table whose primary key sequence will be reset
  507. * @param array|string $value the value for the primary key of the next new row inserted. If this is not set,
  508. * the next new row's primary key will have a value 1.
  509. * @return string the SQL statement for resetting sequence
  510. * @throws NotSupportedException if this is not supported by the underlying DBMS
  511. */
  512. public function resetSequence($table, $value = null)
  513. {
  514. throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
  515. }
  516. /**
  517. * Builds a SQL statement for enabling or disabling integrity check.
  518. * @param boolean $check whether to turn on or off the integrity check.
  519. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
  520. * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
  521. * @return string the SQL statement for checking integrity
  522. * @throws NotSupportedException if this is not supported by the underlying DBMS
  523. */
  524. public function checkIntegrity($check = true, $schema = '', $table = '')
  525. {
  526. throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.');
  527. }
  528. /**
  529. * Builds a SQL command for adding comment to column
  530. *
  531. * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
  532. * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
  533. * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
  534. * @return string the SQL statement for adding comment on column
  535. * @since 2.0.8
  536. */
  537. public function addCommentOnColumn($table, $column, $comment)
  538. {
  539. return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS ' . $this->db->quoteValue($comment);
  540. }
  541. /**
  542. * Builds a SQL command for adding comment to table
  543. *
  544. * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
  545. * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
  546. * @return string the SQL statement for adding comment on table
  547. * @since 2.0.8
  548. */
  549. public function addCommentOnTable($table, $comment)
  550. {
  551. return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
  552. }
  553. /**
  554. * Builds a SQL command for adding comment to column
  555. *
  556. * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
  557. * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
  558. * @return string the SQL statement for adding comment on column
  559. * @since 2.0.8
  560. */
  561. public function dropCommentFromColumn($table, $column)
  562. {
  563. return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS NULL';
  564. }
  565. /**
  566. * Builds a SQL command for adding comment to table
  567. *
  568. * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
  569. * @return string the SQL statement for adding comment on column
  570. * @since 2.0.8
  571. */
  572. public function dropCommentFromTable($table)
  573. {
  574. return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
  575. }
  576. /**
  577. * Converts an abstract column type into a physical column type.
  578. * The conversion is done using the type map specified in [[typeMap]].
  579. * The following abstract column types are supported (using MySQL as an example to explain the corresponding
  580. * physical types):
  581. *
  582. * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
  583. * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY"
  584. * - `unsignedpk`: an unsigned auto-incremental primary key type, will be converted into "int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"
  585. * - `char`: char type, will be converted into "char(1)"
  586. * - `string`: string type, will be converted into "varchar(255)"
  587. * - `text`: a long string type, will be converted into "text"
  588. * - `smallint`: a small integer type, will be converted into "smallint(6)"
  589. * - `integer`: integer type, will be converted into "int(11)"
  590. * - `bigint`: a big integer type, will be converted into "bigint(20)"
  591. * - `boolean`: boolean type, will be converted into "tinyint(1)"
  592. * - `float``: float number type, will be converted into "float"
  593. * - `decimal`: decimal number type, will be converted into "decimal"
  594. * - `datetime`: datetime type, will be converted into "datetime"
  595. * - `timestamp`: timestamp type, will be converted into "timestamp"
  596. * - `time`: time type, will be converted into "time"
  597. * - `date`: date type, will be converted into "date"
  598. * - `money`: money type, will be converted into "decimal(19,4)"
  599. * - `binary`: binary data type, will be converted into "blob"
  600. *
  601. * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only
  602. * the first part will be converted, and the rest of the parts will be appended to the converted result.
  603. * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
  604. *
  605. * For some of the abstract types you can also specify a length or precision constraint
  606. * by appending it in round brackets directly to the type.
  607. * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
  608. * If the underlying DBMS does not support these kind of constraints for a type it will
  609. * be ignored.
  610. *
  611. * If a type cannot be found in [[typeMap]], it will be returned without any change.
  612. * @param string|ColumnSchemaBuilder $type abstract column type
  613. * @return string physical column type.
  614. */
  615. public function getColumnType($type)
  616. {
  617. if ($type instanceof ColumnSchemaBuilder) {
  618. $type = $type->__toString();
  619. }
  620. if (isset($this->typeMap[$type])) {
  621. return $this->typeMap[$type];
  622. } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
  623. if (isset($this->typeMap[$matches[1]])) {
  624. return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
  625. }
  626. } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
  627. if (isset($this->typeMap[$matches[1]])) {
  628. return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
  629. }
  630. }
  631. return $type;
  632. }
  633. /**
  634. * @param array $columns
  635. * @param array $params the binding parameters to be populated
  636. * @param boolean $distinct
  637. * @param string $selectOption
  638. * @return string the SELECT clause built from [[Query::$select]].
  639. */
  640. public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
  641. {
  642. $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
  643. if ($selectOption !== null) {
  644. $select .= ' ' . $selectOption;
  645. }
  646. if (empty($columns)) {
  647. return $select . ' *';
  648. }
  649. foreach ($columns as $i => $column) {
  650. if ($column instanceof Expression) {
  651. if (is_int($i)) {
  652. $columns[$i] = $column->expression;
  653. } else {
  654. $columns[$i] = $column->expression . ' AS ' . $this->db->quoteColumnName($i);
  655. }
  656. $params = array_merge($params, $column->params);
  657. } elseif ($column instanceof Query) {
  658. list($sql, $params) = $this->build($column, $params);
  659. $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
  660. } elseif (is_string($i)) {
  661. if (strpos($column, '(') === false) {
  662. $column = $this->db->quoteColumnName($column);
  663. }
  664. $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
  665. } elseif (strpos($column, '(') === false) {
  666. if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
  667. $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
  668. } else {
  669. $columns[$i] = $this->db->quoteColumnName($column);
  670. }
  671. }
  672. }
  673. return $select . ' ' . implode(', ', $columns);
  674. }
  675. /**
  676. * @param array $tables
  677. * @param array $params the binding parameters to be populated
  678. * @return string the FROM clause built from [[Query::$from]].
  679. */
  680. public function buildFrom($tables, &$params)
  681. {
  682. if (empty($tables)) {
  683. return '';
  684. }
  685. $tables = $this->quoteTableNames($tables, $params);
  686. return 'FROM ' . implode(', ', $tables);
  687. }
  688. /**
  689. * @param array $joins
  690. * @param array $params the binding parameters to be populated
  691. * @return string the JOIN clause built from [[Query::$join]].
  692. * @throws Exception if the $joins parameter is not in proper format
  693. */
  694. public function buildJoin($joins, &$params)
  695. {
  696. if (empty($joins)) {
  697. return '';
  698. }
  699. foreach ($joins as $i => $join) {
  700. if (!is_array($join) || !isset($join[0], $join[1])) {
  701. throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
  702. }
  703. // 0:join type, 1:join table, 2:on-condition (optional)
  704. list ($joinType, $table) = $join;
  705. $tables = $this->quoteTableNames((array) $table, $params);
  706. $table = reset($tables);
  707. $joins[$i] = "$joinType $table";
  708. if (isset($join[2])) {
  709. $condition = $this->buildCondition($join[2], $params);
  710. if ($condition !== '') {
  711. $joins[$i] .= ' ON ' . $condition;
  712. }
  713. }
  714. }
  715. return implode($this->separator, $joins);
  716. }
  717. /**
  718. * Quotes table names passed
  719. *
  720. * @param array $tables
  721. * @param array $params
  722. * @return array
  723. */
  724. private function quoteTableNames($tables, &$params)
  725. {
  726. foreach ($tables as $i => $table) {
  727. if ($table instanceof Query) {
  728. list($sql, $params) = $this->build($table, $params);
  729. $tables[$i] = "($sql) " . $this->db->quoteTableName($i);
  730. } elseif (is_string($i)) {
  731. if (strpos($table, '(') === false) {
  732. $table = $this->db->quoteTableName($table);
  733. }
  734. $tables[$i] = "$table " . $this->db->quoteTableName($i);
  735. } elseif (strpos($table, '(') === false) {
  736. if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
  737. $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
  738. } else {
  739. $tables[$i] = $this->db->quoteTableName($table);
  740. }
  741. }
  742. }
  743. return $tables;
  744. }
  745. /**
  746. * @param string|array $condition
  747. * @param array $params the binding parameters to be populated
  748. * @return string the WHERE clause built from [[Query::$where]].
  749. */
  750. public function buildWhere($condition, &$params)
  751. {
  752. $where = $this->buildCondition($condition, $params);
  753. return $where === '' ? '' : 'WHERE ' . $where;
  754. }
  755. /**
  756. * @param array $columns
  757. * @return string the GROUP BY clause
  758. */
  759. public function buildGroupBy($columns)
  760. {
  761. if (empty($columns)) {
  762. return '';
  763. }
  764. foreach ($columns as $i => $column) {
  765. if ($column instanceof Expression) {
  766. $columns[$i] = $column->expression;
  767. } elseif (strpos($column, '(') === false) {
  768. $columns[$i] = $this->db->quoteColumnName($column);
  769. }
  770. }
  771. return 'GROUP BY ' . implode(', ', $columns);
  772. }
  773. /**
  774. * @param string|array $condition
  775. * @param array $params the binding parameters to be populated
  776. * @return string the HAVING clause built from [[Query::$having]].
  777. */
  778. public function buildHaving($condition, &$params)
  779. {
  780. $having = $this->buildCondition($condition, $params);
  781. return $having === '' ? '' : 'HAVING ' . $having;
  782. }
  783. /**
  784. * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
  785. * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
  786. * @param array $orderBy the order by columns. See [[Query::orderBy]] for more details on how to specify this parameter.
  787. * @param integer $limit the limit number. See [[Query::limit]] for more details.
  788. * @param integer $offset the offset number. See [[Query::offset]] for more details.
  789. * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
  790. */
  791. public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
  792. {
  793. $orderBy = $this->buildOrderBy($orderBy);
  794. if ($orderBy !== '') {
  795. $sql .= $this->separator . $orderBy;
  796. }
  797. $limit = $this->buildLimit($limit, $offset);
  798. if ($limit !== '') {
  799. $sql .= $this->separator . $limit;
  800. }
  801. return $sql;
  802. }
  803. /**
  804. * @param array $columns
  805. * @return string the ORDER BY clause built from [[Query::$orderBy]].
  806. */
  807. public function buildOrderBy($columns)
  808. {
  809. if (empty($columns)) {
  810. return '';
  811. }
  812. $orders = [];
  813. foreach ($columns as $name => $direction) {
  814. if ($direction instanceof Expression) {
  815. $orders[] = $direction->expression;
  816. } else {
  817. $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
  818. }
  819. }
  820. return 'ORDER BY ' . implode(', ', $orders);
  821. }
  822. /**
  823. * @param integer $limit
  824. * @param integer $offset
  825. * @return string the LIMIT and OFFSET clauses
  826. */
  827. public function buildLimit($limit, $offset)
  828. {
  829. $sql = '';
  830. if ($this->hasLimit($limit)) {
  831. $sql = 'LIMIT ' . $limit;
  832. }
  833. if ($this->hasOffset($offset)) {
  834. $sql .= ' OFFSET ' . $offset;
  835. }
  836. return ltrim($sql);
  837. }
  838. /**
  839. * Checks to see if the given limit is effective.
  840. * @param mixed $limit the given limit
  841. * @return boolean whether the limit is effective
  842. */
  843. protected function hasLimit($limit)
  844. {
  845. return ctype_digit((string) $limit);
  846. }
  847. /**
  848. * Checks to see if the given offset is effective.
  849. * @param mixed $offset the given offset
  850. * @return boolean whether the offset is effective
  851. */
  852. protected function hasOffset($offset)
  853. {
  854. $offset = (string) $offset;
  855. return ctype_digit($offset) && $offset !== '0';
  856. }
  857. /**
  858. * @param array $unions
  859. * @param array $params the binding parameters to be populated
  860. * @return string the UNION clause built from [[Query::$union]].
  861. */
  862. public function buildUnion($unions, &$params)
  863. {
  864. if (empty($unions)) {
  865. return '';
  866. }
  867. $result = '';
  868. foreach ($unions as $i => $union) {
  869. $query = $union['query'];
  870. if ($query instanceof Query) {
  871. list($unions[$i]['query'], $params) = $this->build($query, $params);
  872. }
  873. $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
  874. }
  875. return trim($result);
  876. }
  877. /**
  878. * Processes columns and properly quotes them if necessary.
  879. * It will join all columns into a string with comma as separators.
  880. * @param string|array $columns the columns to be processed
  881. * @return string the processing result
  882. */
  883. public function buildColumns($columns)
  884. {
  885. if (!is_array($columns)) {
  886. if (strpos($columns, '(') !== false) {
  887. return $columns;
  888. } else {
  889. $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
  890. }
  891. }
  892. foreach ($columns as $i => $column) {
  893. if ($column instanceof Expression) {
  894. $columns[$i] = $column->expression;
  895. } elseif (strpos($column, '(') === false) {
  896. $columns[$i] = $this->db->quoteColumnName($column);
  897. }
  898. }
  899. return is_array($columns) ? implode(', ', $columns) : $columns;
  900. }
  901. /**
  902. * Parses the condition specification and generates the corresponding SQL expression.
  903. * @param string|array|Expression $condition the condition specification. Please refer to [[Query::where()]]
  904. * on how to specify a condition.
  905. * @param array $params the binding parameters to be populated
  906. * @return string the generated SQL expression
  907. */
  908. public function buildCondition($condition, &$params)
  909. {
  910. if ($condition instanceof Expression) {
  911. foreach ($condition->params as $n => $v) {
  912. $params[$n] = $v;
  913. }
  914. return $condition->expression;
  915. } elseif (!is_array($condition)) {
  916. return (string) $condition;
  917. } elseif (empty($condition)) {
  918. return '';
  919. }
  920. if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
  921. $operator = strtoupper($condition[0]);
  922. if (isset($this->conditionBuilders[$operator])) {
  923. $method = $this->conditionBuilders[$operator];
  924. } else {
  925. $method = 'buildSimpleCondition';
  926. }
  927. array_shift($condition);
  928. return $this->$method($operator, $condition, $params);
  929. } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
  930. return $this->buildHashCondition($condition, $params);
  931. }
  932. }
  933. /**
  934. * Creates a condition based on column-value pairs.
  935. * @param array $condition the condition specification.
  936. * @param array $params the binding parameters to be populated
  937. * @return string the generated SQL expression
  938. */
  939. public function buildHashCondition($condition, &$params)
  940. {
  941. $parts = [];
  942. foreach ($condition as $column => $value) {
  943. if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
  944. // IN condition
  945. $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
  946. } else {
  947. if (strpos($column, '(') === false) {
  948. $column = $this->db->quoteColumnName($column);
  949. }
  950. if ($value === null) {
  951. $parts[] = "$column IS NULL";
  952. } elseif ($value instanceof Expression) {
  953. $parts[] = "$column=" . $value->expression;
  954. foreach ($value->params as $n => $v) {
  955. $params[$n] = $v;
  956. }
  957. } else {
  958. $phName = self::PARAM_PREFIX . count($params);
  959. $parts[] = "$column=$phName";
  960. $params[$phName] = $value;
  961. }
  962. }
  963. }
  964. return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
  965. }
  966. /**
  967. * Connects two or more SQL expressions with the `AND` or `OR` operator.
  968. * @param string $operator the operator to use for connecting the given operands
  969. * @param array $operands the SQL expressions to connect.
  970. * @param array $params the binding parameters to be populated
  971. * @return string the generated SQL expression
  972. */
  973. public function buildAndCondition($operator, $operands, &$params)
  974. {
  975. $parts = [];
  976. foreach ($operands as $operand) {
  977. if (is_array($operand)) {
  978. $operand = $this->buildCondition($operand, $params);
  979. }
  980. if ($operand instanceof Expression) {
  981. foreach ($operand->params as $n => $v) {
  982. $params[$n] = $v;
  983. }
  984. $operand = $operand->expression;
  985. }
  986. if ($operand !== '') {
  987. $parts[] = $operand;
  988. }
  989. }
  990. if (!empty($parts)) {
  991. return '(' . implode(") $operator (", $parts) . ')';
  992. } else {
  993. return '';
  994. }
  995. }
  996. /**
  997. * Inverts an SQL expressions with `NOT` operator.
  998. * @param string $operator the operator to use for connecting the given operands
  999. * @param array $operands the SQL expressions to connect.
  1000. * @param array $params the binding parameters to be populated
  1001. * @return string the generated SQL expression
  1002. * @throws InvalidParamException if wrong number of operands have been given.
  1003. */
  1004. public function buildNotCondition($operator, $operands, &$params)
  1005. {
  1006. if (count($operands) !== 1) {
  1007. throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
  1008. }
  1009. $operand = reset($operands);
  1010. if (is_array($operand)) {
  1011. $operand = $this->buildCondition($operand, $params);
  1012. }
  1013. if ($operand === '') {
  1014. return '';
  1015. }
  1016. return "$operator ($operand)";
  1017. }
  1018. /**
  1019. * Creates an SQL expressions with the `BETWEEN` operator.
  1020. * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
  1021. * @param array $operands the first operand is the column name. The second and third operands
  1022. * describe the interval that column value should be in.
  1023. * @param array $params the binding parameters to be populated
  1024. * @return string the generated SQL expression
  1025. * @throws InvalidParamException if wrong number of operands have been given.
  1026. */
  1027. public function buildBetweenCondition($operator, $operands, &$params)
  1028. {
  1029. if (!isset($operands[0], $operands[1], $operands[2])) {
  1030. throw new InvalidParamException("Operator '$operator' requires three operands.");
  1031. }
  1032. list($column, $value1, $value2) = $operands;
  1033. if (strpos($column, '(') === false) {
  1034. $column = $this->db->quoteColumnName($column);
  1035. }
  1036. if ($value1 instanceof Expression) {
  1037. foreach ($value1->params as $n => $v) {
  1038. $params[$n] = $v;
  1039. }
  1040. $phName1 = $value1->expression;
  1041. } else {
  1042. $phName1 = self::PARAM_PREFIX . count($params);
  1043. $params[$phName1] = $value1;
  1044. }
  1045. if ($value2 instanceof Expression) {
  1046. foreach ($value2->params as $n => $v) {
  1047. $params[$n] = $v;
  1048. }
  1049. $phName2 = $value2->expression;
  1050. } else {
  1051. $phName2 = self::PARAM_PREFIX . count($params);
  1052. $params[$phName2] = $value2;
  1053. }
  1054. return "$column $operator $phName1 AND $phName2";
  1055. }
  1056. /**
  1057. * Creates an SQL expressions with the `IN` operator.
  1058. * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
  1059. * @param array $operands the first operand is the column name. If it is an array
  1060. * a composite IN condition will be generated.
  1061. * The second operand is an array of values that column value should be among.
  1062. * If it is an empty array the generated expression will be a `false` value if
  1063. * operator is `IN` and empty if operator is `NOT IN`.
  1064. * @param array $params the binding parameters to be populated
  1065. * @return string the generated SQL expression
  1066. * @throws Exception if wrong number of operands have been given.
  1067. */
  1068. public function buildInCondition($operator, $operands, &$params)
  1069. {
  1070. if (!isset($operands[0], $operands[1])) {
  1071. throw new Exception("Operator '$operator' requires two operands.");
  1072. }
  1073. list($column, $values) = $operands;
  1074. if ($column === []) {
  1075. // no columns to test against
  1076. return $operator === 'IN' ? '0=1' : '';
  1077. }
  1078. if ($values instanceof Query) {
  1079. return $this->buildSubqueryInCondition($operator, $column, $values, $params);
  1080. }
  1081. if (!is_array($values) && !$values instanceof \Traversable) {
  1082. // ensure values is an array
  1083. $values = (array) $values;
  1084. }
  1085. if ($column instanceof \Traversable || count($column) > 1) {
  1086. return $this->buildCompositeInCondition($operator, $column, $values, $params);
  1087. } elseif (is_array($column)) {
  1088. $column = reset($column);
  1089. }
  1090. $sqlValues = [];
  1091. foreach ($values as $i => $value) {
  1092. if (is_array($value) || $value instanceof \ArrayAccess) {
  1093. $value = isset($value[$column]) ? $value[$column] : null;
  1094. }
  1095. if ($value === null) {
  1096. $sqlValues[$i] = 'NULL';
  1097. } elseif ($value instanceof Expression) {
  1098. $sqlValues[$i] = $value->expression;
  1099. foreach ($value->params as $n => $v) {
  1100. $params[$n] = $v;
  1101. }
  1102. } else {
  1103. $phName = self::PARAM_PREFIX . count($params);
  1104. $params[$phName] = $value;
  1105. $sqlValues[$i] = $phName;
  1106. }
  1107. }
  1108. if (empty($sqlValues)) {
  1109. return $operator === 'IN' ? '0=1' : '';
  1110. }
  1111. if (strpos($column, '(') === false) {
  1112. $column = $this->db->quoteColumnName($column);
  1113. }
  1114. if (count($sqlValues) > 1) {
  1115. return "$column $operator (" . implode(', ', $sqlValues) . ')';
  1116. } else {
  1117. $operator = $operator === 'IN' ? '=' : '<>';
  1118. return $column . $operator . reset($sqlValues);
  1119. }
  1120. }
  1121. /**
  1122. * Builds SQL for IN condition
  1123. *
  1124. * @param string $operator
  1125. * @param array $columns
  1126. * @param Query $values
  1127. * @param array $params
  1128. * @return string SQL
  1129. */
  1130. protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
  1131. {
  1132. list($sql, $params) = $this->build($values, $params);
  1133. if (is_array($columns)) {
  1134. foreach ($columns as $i => $col) {
  1135. if (strpos($col, '(') === false) {
  1136. $columns[$i] = $this->db->quoteColumnName($col);
  1137. }
  1138. }
  1139. return '(' . implode(', ', $columns) . ") $operator ($sql)";
  1140. } else {
  1141. if (strpos($columns, '(') === false) {
  1142. $columns = $this->db->quoteColumnName($columns);
  1143. }
  1144. return "$columns $operator ($sql)";
  1145. }
  1146. }
  1147. /**
  1148. * Builds SQL for IN condition
  1149. *
  1150. * @param string $operator
  1151. * @param array|\Traversable $columns
  1152. * @param array $values
  1153. * @param array $params
  1154. * @return string SQL
  1155. */
  1156. protected function buildCompositeInCondition($operator, $columns, $values, &$params)
  1157. {
  1158. $vss = [];
  1159. foreach ($values as $value) {
  1160. $vs = [];
  1161. foreach ($columns as $column) {
  1162. if (isset($value[$column])) {
  1163. $phName = self::PARAM_PREFIX . count($params);
  1164. $params[$phName] = $value[$column];
  1165. $vs[] = $phName;
  1166. } else {
  1167. $vs[] = 'NULL';
  1168. }
  1169. }
  1170. $vss[] = '(' . implode(', ', $vs) . ')';
  1171. }
  1172. if (empty($vss)) {
  1173. return $operator === 'IN' ? '0=1' : '';
  1174. };
  1175. $sqlColumns = [];
  1176. foreach ($columns as $i => $column) {
  1177. $sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
  1178. }
  1179. return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
  1180. }
  1181. /**
  1182. * Creates an SQL expressions with the `LIKE` operator.
  1183. * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
  1184. * @param array $operands an array of two or three operands
  1185. *
  1186. * - The first operand is the column name.
  1187. * - The second operand is a single value or an array of values that column value
  1188. * should be compared with. If it is an empty array the generated expression will
  1189. * be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
  1190. * is `NOT LIKE` or `OR NOT LIKE`.
  1191. * - An optional third operand can also be provided to specify how to escape special characters
  1192. * in the value(s). The operand should be an array of mappings from the special characters to their
  1193. * escaped counterparts. If this operand is not provided, a default escape mapping will be used.
  1194. * You may use `false` or an empty array to indicate the values are already escaped and no escape
  1195. * should be applied. Note that when using an escape mapping (or the third operand is not provided),
  1196. * the values will be automatically enclosed within a pair of percentage characters.
  1197. * @param array $params the binding parameters to be populated
  1198. * @return string the generated SQL expression
  1199. * @throws InvalidParamException if wrong number of operands have been given.
  1200. */
  1201. public function buildLikeCondition($operator, $operands, &$params)
  1202. {
  1203. if (!isset($operands[0], $operands[1])) {
  1204. throw new InvalidParamException("Operator '$operator' requires two operands.");
  1205. }
  1206. $escape = isset($operands[2]) ? $operands[2] : ['%' => '\%', '_' => '\_', '\\' => '\\\\'];
  1207. unset($operands[2]);
  1208. if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
  1209. throw new InvalidParamException("Invalid operator '$operator'.");
  1210. }
  1211. $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
  1212. $not = !empty($matches[3]);
  1213. $operator = $matches[2];
  1214. list($column, $values) = $operands;
  1215. if (!is_array($values)) {
  1216. $values = [$values];
  1217. }
  1218. if (empty($values)) {
  1219. return $not ? '' : '0=1';
  1220. }
  1221. if (strpos($column, '(') === false) {
  1222. $column = $this->db->quoteColumnName($column);
  1223. }
  1224. $parts = [];
  1225. foreach ($values as $value) {
  1226. if ($value instanceof Expression) {
  1227. foreach ($value->params as $n => $v) {
  1228. $params[$n] = $v;
  1229. }
  1230. $phName = $value->expression;
  1231. } else {
  1232. $phName = self::PARAM_PREFIX . count($params);
  1233. $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
  1234. }
  1235. $parts[] = "$column $operator $phName";
  1236. }
  1237. return implode($andor, $parts);
  1238. }
  1239. /**
  1240. * Creates an SQL expressions with the `EXISTS` operator.
  1241. * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
  1242. * @param array $operands contains only one element which is a [[Query]] object representing the sub-query.
  1243. * @param array $params the binding parameters to be populated
  1244. * @return string the generated SQL expression
  1245. * @throws InvalidParamException if the operand is not a [[Query]] object.
  1246. */
  1247. public function buildExistsCondition($operator, $operands, &$params)
  1248. {
  1249. if ($operands[0] instanceof Query) {
  1250. list($sql, $params) = $this->build($operands[0], $params);
  1251. return "$operator ($sql)";
  1252. } else {
  1253. throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
  1254. }
  1255. }
  1256. /**
  1257. * Creates an SQL expressions like `"column" operator value`.
  1258. * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
  1259. * @param array $operands contains two column names.
  1260. * @param array $params the binding parameters to be populated
  1261. * @return string the generated SQL expression
  1262. * @throws InvalidParamException if wrong number of operands have been given.
  1263. */
  1264. public function buildSimpleCondition($operator, $operands, &$params)
  1265. {
  1266. if (count($operands) !== 2) {
  1267. throw new InvalidParamException("Operator '$operator' requires two operands.");
  1268. }
  1269. list($column, $value) = $operands;
  1270. if (strpos($column, '(') === false) {
  1271. $column = $this->db->quoteColumnName($column);
  1272. }
  1273. if ($value === null) {
  1274. return "$column $operator NULL";
  1275. } elseif ($value instanceof Expression) {
  1276. foreach ($value->params as $n => $v) {
  1277. $params[$n] = $v;
  1278. }
  1279. return "$column $operator {$value->expression}";
  1280. } elseif ($value instanceof Query) {
  1281. list($sql, $params) = $this->build($value, $params);
  1282. return "$column $operator ($sql)";
  1283. } else {
  1284. $phName = self::PARAM_PREFIX . count($params);
  1285. $params[$phName] = $value;
  1286. return "$column $operator $phName";
  1287. }
  1288. }
  1289. /**
  1290. * Creates a SELECT EXISTS() SQL statement.
  1291. * @param string $rawSql the subquery in a raw form to select from.
  1292. * @return string the SELECT EXISTS() SQL statement.
  1293. * @since 2.0.8
  1294. */
  1295. public function selectExists($rawSql)
  1296. {
  1297. return 'SELECT EXISTS(' . $rawSql . ')';
  1298. }
  1299. }