QueryBuilder.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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\oci;
  8. use yii\base\InvalidParamException;
  9. use yii\db\Connection;
  10. use yii\db\Exception;
  11. use yii\db\Expression;
  12. /**
  13. * QueryBuilder is the query builder for Oracle databases.
  14. *
  15. * @author Qiang Xue <qiang.xue@gmail.com>
  16. * @since 2.0
  17. */
  18. class QueryBuilder extends \yii\db\QueryBuilder
  19. {
  20. /**
  21. * @var array mapping from abstract column types (keys) to physical column types (values).
  22. */
  23. public $typeMap = [
  24. Schema::TYPE_PK => 'NUMBER(10) NOT NULL PRIMARY KEY',
  25. Schema::TYPE_UPK => 'NUMBER(10) UNSIGNED NOT NULL PRIMARY KEY',
  26. Schema::TYPE_BIGPK => 'NUMBER(20) NOT NULL PRIMARY KEY',
  27. Schema::TYPE_UBIGPK => 'NUMBER(20) UNSIGNED NOT NULL PRIMARY KEY',
  28. Schema::TYPE_CHAR => 'CHAR(1)',
  29. Schema::TYPE_STRING => 'VARCHAR2(255)',
  30. Schema::TYPE_TEXT => 'CLOB',
  31. Schema::TYPE_SMALLINT => 'NUMBER(5)',
  32. Schema::TYPE_INTEGER => 'NUMBER(10)',
  33. Schema::TYPE_BIGINT => 'NUMBER(20)',
  34. Schema::TYPE_FLOAT => 'NUMBER',
  35. Schema::TYPE_DOUBLE => 'NUMBER',
  36. Schema::TYPE_DECIMAL => 'NUMBER',
  37. Schema::TYPE_DATETIME => 'TIMESTAMP',
  38. Schema::TYPE_TIMESTAMP => 'TIMESTAMP',
  39. Schema::TYPE_TIME => 'TIMESTAMP',
  40. Schema::TYPE_DATE => 'DATE',
  41. Schema::TYPE_BINARY => 'BLOB',
  42. Schema::TYPE_BOOLEAN => 'NUMBER(1)',
  43. Schema::TYPE_MONEY => 'NUMBER(19,4)',
  44. ];
  45. /**
  46. * @inheritdoc
  47. */
  48. protected $likeEscapeCharacter = '!';
  49. /**
  50. * `\` is initialized in [[buildLikeCondition()]] method since
  51. * we need to choose replacement value based on [[\yii\db\Schema::quoteValue()]].
  52. * @inheritdoc
  53. */
  54. protected $likeEscapingReplacements = [
  55. '%' => '!%',
  56. '_' => '!_',
  57. '!' => '!!',
  58. ];
  59. /**
  60. * @inheritdoc
  61. */
  62. public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
  63. {
  64. $orderBy = $this->buildOrderBy($orderBy);
  65. if ($orderBy !== '') {
  66. $sql .= $this->separator . $orderBy;
  67. }
  68. $filters = [];
  69. if ($this->hasOffset($offset)) {
  70. $filters[] = 'rowNumId > ' . $offset;
  71. }
  72. if ($this->hasLimit($limit)) {
  73. $filters[] = 'rownum <= ' . $limit;
  74. }
  75. if (empty($filters)) {
  76. return $sql;
  77. }
  78. $filter = implode(' AND ', $filters);
  79. return <<<EOD
  80. WITH USER_SQL AS ($sql),
  81. PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL)
  82. SELECT *
  83. FROM PAGINATION
  84. WHERE $filter
  85. EOD;
  86. }
  87. /**
  88. * Builds a SQL statement for renaming a DB table.
  89. *
  90. * @param string $table the table to be renamed. The name will be properly quoted by the method.
  91. * @param string $newName the new table name. The name will be properly quoted by the method.
  92. * @return string the SQL statement for renaming a DB table.
  93. */
  94. public function renameTable($table, $newName)
  95. {
  96. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
  97. }
  98. /**
  99. * Builds a SQL statement for changing the definition of a column.
  100. *
  101. * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
  102. * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
  103. * @param string $type the new column type. The [[getColumnType]] method will be invoked to convert abstract column type (if any)
  104. * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
  105. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
  106. * @return string the SQL statement for changing the definition of a column.
  107. */
  108. public function alterColumn($table, $column, $type)
  109. {
  110. $type = $this->getColumnType($type);
  111. return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' MODIFY ' . $this->db->quoteColumnName($column) . ' ' . $this->getColumnType($type);
  112. }
  113. /**
  114. * Builds a SQL statement for dropping an index.
  115. *
  116. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
  117. * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
  118. * @return string the SQL statement for dropping an index.
  119. */
  120. public function dropIndex($name, $table)
  121. {
  122. return 'DROP INDEX ' . $this->db->quoteTableName($name);
  123. }
  124. /**
  125. * @inheritdoc
  126. */
  127. public function resetSequence($table, $value = null)
  128. {
  129. $tableSchema = $this->db->getTableSchema($table);
  130. if ($tableSchema === null) {
  131. throw new InvalidParamException("Unknown table: $table");
  132. }
  133. if ($tableSchema->sequenceName === null) {
  134. return '';
  135. }
  136. if ($value !== null) {
  137. $value = (int) $value;
  138. } else {
  139. // use master connection to get the biggest PK value
  140. $value = $this->db->useMaster(function (Connection $db) use ($tableSchema) {
  141. return $db->createCommand("SELECT MAX(\"{$tableSchema->primaryKey}\") FROM \"{$tableSchema->name}\"")->queryScalar();
  142. }) + 1;
  143. }
  144. return "DROP SEQUENCE \"{$tableSchema->name}_SEQ\";"
  145. . "CREATE SEQUENCE \"{$tableSchema->name}_SEQ\" START WITH {$value} INCREMENT BY 1 NOMAXVALUE NOCACHE";
  146. }
  147. /**
  148. * @inheritdoc
  149. */
  150. public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
  151. {
  152. $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
  153. . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
  154. . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
  155. . ' REFERENCES ' . $this->db->quoteTableName($refTable)
  156. . ' (' . $this->buildColumns($refColumns) . ')';
  157. if ($delete !== null) {
  158. $sql .= ' ON DELETE ' . $delete;
  159. }
  160. if ($update !== null) {
  161. throw new Exception('Oracle does not support ON UPDATE clause.');
  162. }
  163. return $sql;
  164. }
  165. /**
  166. * @inheritdoc
  167. */
  168. public function insert($table, $columns, &$params)
  169. {
  170. $schema = $this->db->getSchema();
  171. if (($tableSchema = $schema->getTableSchema($table)) !== null) {
  172. $columnSchemas = $tableSchema->columns;
  173. } else {
  174. $columnSchemas = [];
  175. }
  176. $names = [];
  177. $placeholders = [];
  178. $values = ' DEFAULT VALUES';
  179. if ($columns instanceof \yii\db\Query) {
  180. list($names, $values, $params) = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
  181. } else {
  182. foreach ($columns as $name => $value) {
  183. $names[] = $schema->quoteColumnName($name);
  184. if ($value instanceof Expression) {
  185. $placeholders[] = $value->expression;
  186. foreach ($value->params as $n => $v) {
  187. $params[$n] = $v;
  188. }
  189. } elseif ($value instanceof \yii\db\Query) {
  190. list($sql, $params) = $this->build($value, $params);
  191. $placeholders[] = "($sql)";
  192. } else {
  193. $phName = self::PARAM_PREFIX . count($params);
  194. $placeholders[] = $phName;
  195. $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
  196. }
  197. }
  198. if (empty($names) && $tableSchema !== null) {
  199. $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name];
  200. foreach ($columns as $name) {
  201. $names[] = $schema->quoteColumnName($name);
  202. $placeholders[] = 'DEFAULT';
  203. }
  204. }
  205. }
  206. return 'INSERT INTO ' . $schema->quoteTableName($table)
  207. . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
  208. . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
  209. }
  210. /**
  211. * Generates a batch INSERT SQL statement.
  212. * For example,
  213. *
  214. * ```php
  215. * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
  216. * ['Tom', 30],
  217. * ['Jane', 20],
  218. * ['Linda', 25],
  219. * ]);
  220. * ```
  221. *
  222. * Note that the values in each row must match the corresponding column names.
  223. *
  224. * @param string $table the table that new rows will be inserted into.
  225. * @param array $columns the column names
  226. * @param array $rows the rows to be batch inserted into the table
  227. * @return string the batch INSERT SQL statement
  228. */
  229. public function batchInsert($table, $columns, $rows)
  230. {
  231. if (empty($rows)) {
  232. return '';
  233. }
  234. $schema = $this->db->getSchema();
  235. if (($tableSchema = $schema->getTableSchema($table)) !== null) {
  236. $columnSchemas = $tableSchema->columns;
  237. } else {
  238. $columnSchemas = [];
  239. }
  240. $values = [];
  241. foreach ($rows as $row) {
  242. $vs = [];
  243. foreach ($row as $i => $value) {
  244. if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
  245. $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
  246. }
  247. if (is_string($value)) {
  248. $value = $schema->quoteValue($value);
  249. } elseif ($value === false) {
  250. $value = 0;
  251. } elseif ($value === null) {
  252. $value = 'NULL';
  253. }
  254. $vs[] = $value;
  255. }
  256. $values[] = '(' . implode(', ', $vs) . ')';
  257. }
  258. if (empty($values)) {
  259. return '';
  260. }
  261. foreach ($columns as $i => $name) {
  262. $columns[$i] = $schema->quoteColumnName($name);
  263. }
  264. $tableAndColumns = ' INTO ' . $schema->quoteTableName($table)
  265. . ' (' . implode(', ', $columns) . ') VALUES ';
  266. return 'INSERT ALL ' . $tableAndColumns . implode($tableAndColumns, $values) . ' SELECT 1 FROM SYS.DUAL';
  267. }
  268. /**
  269. * @inheritdoc
  270. * @since 2.0.8
  271. */
  272. public function selectExists($rawSql)
  273. {
  274. return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END FROM DUAL';
  275. }
  276. /**
  277. * @inheritdoc
  278. * @since 2.0.8
  279. */
  280. public function dropCommentFromColumn($table, $column)
  281. {
  282. return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . " IS ''";
  283. }
  284. /**
  285. * @inheritdoc
  286. * @since 2.0.8
  287. */
  288. public function dropCommentFromTable($table)
  289. {
  290. return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . " IS ''";
  291. }
  292. /**
  293. * @inheritDoc
  294. */
  295. public function buildLikeCondition($operator, $operands, &$params)
  296. {
  297. if (!isset($this->likeEscapingReplacements['\\'])) {
  298. /*
  299. * Different pdo_oci8 versions may or may not implement PDO::quote(), so
  300. * yii\db\Schema::quoteValue() may or may not quote \.
  301. */
  302. $this->likeEscapingReplacements['\\'] = substr($this->db->quoteValue('\\'), 1, -1);
  303. }
  304. return parent::buildLikeCondition($operator, $operands, $params);
  305. }
  306. /**
  307. * @inheritdoc
  308. */
  309. public function buildInCondition($operator, $operands, &$params)
  310. {
  311. $splitCondition = $this->splitInCondition($operator, $operands, $params);
  312. if ($splitCondition !== null) {
  313. return $splitCondition;
  314. }
  315. return parent::buildInCondition($operator, $operands, $params);
  316. }
  317. /**
  318. * Oracle DBMS does not support more than 1000 parameters in `IN` condition.
  319. * This method splits long `IN` condition into series of smaller ones.
  320. *
  321. * @param string $operator
  322. * @param array $operands
  323. * @param array $params
  324. * @return null|string null when split is not required. Otherwise - built SQL condition.
  325. * @throws Exception
  326. * @since 2.0.12
  327. */
  328. protected function splitInCondition($operator, $operands, &$params)
  329. {
  330. if (!isset($operands[0], $operands[1])) {
  331. throw new Exception("Operator '$operator' requires two operands.");
  332. }
  333. list($column, $values) = $operands;
  334. if ($values instanceof \Traversable) {
  335. $values = iterator_to_array($values);
  336. }
  337. if (!is_array($values)) {
  338. return null;
  339. }
  340. $maxParameters = 1000;
  341. $count = count($values);
  342. if ($count <= $maxParameters) {
  343. return null;
  344. }
  345. $condition = [($operator === 'IN') ? 'OR' : 'AND'];
  346. for ($i = 0; $i < $count; $i += $maxParameters) {
  347. $condition[] = [$operator, $column, array_slice($values, $i, $maxParameters)];
  348. }
  349. return $this->buildCondition(['AND', $condition], $params);
  350. }
  351. }