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.

496 lines
16KB

  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\InvalidCallException;
  9. use yii\db\ColumnSchema;
  10. use yii\db\Connection;
  11. use yii\db\Expression;
  12. use yii\db\TableSchema;
  13. /**
  14. * Schema is the class for retrieving metadata from an Oracle database
  15. *
  16. * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
  17. * sequence object. This property is read-only.
  18. *
  19. * @author Qiang Xue <qiang.xue@gmail.com>
  20. * @since 2.0
  21. */
  22. class Schema extends \yii\db\Schema
  23. {
  24. /**
  25. * @var array map of DB errors and corresponding exceptions
  26. * If left part is found in DB error message exception class from the right part is used.
  27. */
  28. public $exceptionMap = [
  29. 'ORA-00001: unique constraint' => 'yii\db\IntegrityException',
  30. ];
  31. /**
  32. * @inheritdoc
  33. */
  34. public function init()
  35. {
  36. parent::init();
  37. if ($this->defaultSchema === null) {
  38. $this->defaultSchema = strtoupper($this->db->username);
  39. }
  40. }
  41. /**
  42. * @inheritdoc
  43. */
  44. public function releaseSavepoint($name)
  45. {
  46. // does nothing as Oracle does not support this
  47. }
  48. /**
  49. * @inheritdoc
  50. */
  51. public function quoteSimpleTableName($name)
  52. {
  53. return strpos($name, '"') !== false ? $name : '"' . $name . '"';
  54. }
  55. /**
  56. * @inheritdoc
  57. */
  58. public function createQueryBuilder()
  59. {
  60. return new QueryBuilder($this->db);
  61. }
  62. /**
  63. * @inheritdoc
  64. */
  65. public function createColumnSchemaBuilder($type, $length = null)
  66. {
  67. return new ColumnSchemaBuilder($type, $length, $this->db);
  68. }
  69. /**
  70. * @inheritdoc
  71. */
  72. public function loadTableSchema($name)
  73. {
  74. $table = new TableSchema();
  75. $this->resolveTableNames($table, $name);
  76. if ($this->findColumns($table)) {
  77. $this->findConstraints($table);
  78. return $table;
  79. } else {
  80. return null;
  81. }
  82. }
  83. /**
  84. * Resolves the table name and schema name (if any).
  85. *
  86. * @param TableSchema $table the table metadata object
  87. * @param string $name the table name
  88. */
  89. protected function resolveTableNames($table, $name)
  90. {
  91. $parts = explode('.', str_replace('"', '', $name));
  92. if (isset($parts[1])) {
  93. $table->schemaName = $parts[0];
  94. $table->name = $parts[1];
  95. } else {
  96. $table->schemaName = $this->defaultSchema;
  97. $table->name = $name;
  98. }
  99. $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
  100. }
  101. /**
  102. * Collects the table column metadata.
  103. * @param TableSchema $table the table schema
  104. * @return boolean whether the table exists
  105. */
  106. protected function findColumns($table)
  107. {
  108. $sql = <<<SQL
  109. SELECT a.column_name, a.data_type, a.data_precision, a.data_scale, a.data_length,
  110. a.nullable, a.data_default,
  111. com.comments as column_comment
  112. FROM ALL_TAB_COLUMNS A
  113. inner join ALL_OBJECTS B ON b.owner = a.owner and ltrim(B.OBJECT_NAME) = ltrim(A.TABLE_NAME)
  114. LEFT JOIN all_col_comments com ON (A.owner = com.owner AND A.table_name = com.table_name AND A.column_name = com.column_name)
  115. WHERE
  116. a.owner = :schemaName
  117. and b.object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
  118. and b.object_name = :tableName
  119. ORDER by a.column_id
  120. SQL;
  121. try {
  122. $columns = $this->db->createCommand($sql, [
  123. ':tableName' => $table->name,
  124. ':schemaName' => $table->schemaName,
  125. ])->queryAll();
  126. } catch (\Exception $e) {
  127. return false;
  128. }
  129. if (empty($columns)) {
  130. return false;
  131. }
  132. foreach ($columns as $column) {
  133. if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) {
  134. $column = array_change_key_case($column, CASE_UPPER);
  135. }
  136. $c = $this->createColumn($column);
  137. $table->columns[$c->name] = $c;
  138. }
  139. return true;
  140. }
  141. /**
  142. * Sequence name of table
  143. *
  144. * @param string $tableName
  145. * @internal param \yii\db\TableSchema $table->name the table schema
  146. * @return string|null whether the sequence exists
  147. */
  148. protected function getTableSequenceName($tableName)
  149. {
  150. $seq_name_sql = <<<SQL
  151. SELECT ud.referenced_name as sequence_name
  152. FROM user_dependencies ud
  153. JOIN user_triggers ut on (ut.trigger_name = ud.name)
  154. WHERE ut.table_name = :tableName
  155. AND ud.type='TRIGGER'
  156. AND ud.referenced_type='SEQUENCE'
  157. SQL;
  158. $sequenceName = $this->db->createCommand($seq_name_sql, [':tableName' => $tableName])->queryScalar();
  159. return $sequenceName === false ? null : $sequenceName;
  160. }
  161. /**
  162. * @Overrides method in class 'Schema'
  163. * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php -> Oracle does not support this
  164. *
  165. * Returns the ID of the last inserted row or sequence value.
  166. * @param string $sequenceName name of the sequence object (required by some DBMS)
  167. * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
  168. * @throws InvalidCallException if the DB connection is not active
  169. */
  170. public function getLastInsertID($sequenceName = '')
  171. {
  172. if ($this->db->isActive) {
  173. // get the last insert id from the master connection
  174. $sequenceName = $this->quoteSimpleTableName($sequenceName);
  175. return $this->db->useMaster(function (Connection $db) use ($sequenceName) {
  176. return $db->createCommand("SELECT {$sequenceName}.CURRVAL FROM DUAL")->queryScalar();
  177. });
  178. } else {
  179. throw new InvalidCallException('DB Connection is not active.');
  180. }
  181. }
  182. /**
  183. * Creates ColumnSchema instance
  184. *
  185. * @param array $column
  186. * @return ColumnSchema
  187. */
  188. protected function createColumn($column)
  189. {
  190. $c = $this->createColumnSchema();
  191. $c->name = $column['COLUMN_NAME'];
  192. $c->allowNull = $column['NULLABLE'] === 'Y';
  193. $c->comment = $column['COLUMN_COMMENT'] === null ? '' : $column['COLUMN_COMMENT'];
  194. $c->isPrimaryKey = false;
  195. $this->extractColumnType($c, $column['DATA_TYPE'], $column['DATA_PRECISION'], $column['DATA_SCALE'], $column['DATA_LENGTH']);
  196. $this->extractColumnSize($c, $column['DATA_TYPE'], $column['DATA_PRECISION'], $column['DATA_SCALE'], $column['DATA_LENGTH']);
  197. $c->phpType = $this->getColumnPhpType($c);
  198. if (!$c->isPrimaryKey) {
  199. if (stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
  200. $c->defaultValue = null;
  201. } else {
  202. $defaultValue = $column['DATA_DEFAULT'];
  203. if ($c->type === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
  204. $c->defaultValue = new Expression('CURRENT_TIMESTAMP');
  205. } else {
  206. if ($defaultValue !== null) {
  207. if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
  208. && $defaultValue[$len - 1] === "'"
  209. ) {
  210. $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
  211. } else {
  212. $defaultValue = trim($defaultValue);
  213. }
  214. }
  215. $c->defaultValue = $c->phpTypecast($defaultValue);
  216. }
  217. }
  218. }
  219. return $c;
  220. }
  221. /**
  222. * Finds constraints and fills them into TableSchema object passed
  223. * @param TableSchema $table
  224. */
  225. protected function findConstraints($table)
  226. {
  227. $sql = <<<SQL
  228. SELECT D.CONSTRAINT_NAME, D.CONSTRAINT_TYPE, C.COLUMN_NAME, C.POSITION, D.R_CONSTRAINT_NAME,
  229. E.TABLE_NAME AS TABLE_REF, F.COLUMN_NAME AS COLUMN_REF,
  230. C.TABLE_NAME
  231. FROM ALL_CONS_COLUMNS C
  232. INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
  233. LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
  234. LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
  235. WHERE C.OWNER = :schemaName
  236. AND C.TABLE_NAME = :tableName
  237. ORDER BY D.CONSTRAINT_NAME, C.POSITION
  238. SQL;
  239. $command = $this->db->createCommand($sql, [
  240. ':tableName' => $table->name,
  241. ':schemaName' => $table->schemaName,
  242. ]);
  243. $constraints = [];
  244. foreach ($command->queryAll() as $row) {
  245. if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) {
  246. $row = array_change_key_case($row, CASE_UPPER);
  247. }
  248. if ($row['CONSTRAINT_TYPE'] === 'P') {
  249. $table->columns[$row['COLUMN_NAME']]->isPrimaryKey = true;
  250. $table->primaryKey[] = $row['COLUMN_NAME'];
  251. if (empty($table->sequenceName)) {
  252. $table->sequenceName = $this->getTableSequenceName($table->name);
  253. }
  254. }
  255. if ($row['CONSTRAINT_TYPE'] !== 'R') {
  256. // this condition is not checked in SQL WHERE because of an Oracle Bug:
  257. // see https://github.com/yiisoft/yii2/pull/8844
  258. continue;
  259. }
  260. $name = $row['CONSTRAINT_NAME'];
  261. if (!isset($constraints[$name])) {
  262. $constraints[$name] = [
  263. 'tableName' => $row['TABLE_REF'],
  264. 'columns' => [],
  265. ];
  266. }
  267. $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
  268. }
  269. foreach ($constraints as $constraint) {
  270. $table->foreignKeys[] = array_merge([$constraint['tableName']], $constraint['columns']);
  271. }
  272. }
  273. /**
  274. * @inheritdoc
  275. */
  276. protected function findSchemaNames()
  277. {
  278. $sql = <<<SQL
  279. SELECT username
  280. FROM dba_users u
  281. WHERE EXISTS (
  282. SELECT 1
  283. FROM dba_objects o
  284. WHERE o.owner = u.username )
  285. AND default_tablespace not in ('SYSTEM','SYSAUX')
  286. SQL;
  287. return $this->db->createCommand($sql)->queryColumn();
  288. }
  289. /**
  290. * @inheritdoc
  291. */
  292. protected function findTableNames($schema = '')
  293. {
  294. if ($schema === '') {
  295. $sql = <<<SQL
  296. SELECT table_name FROM user_tables
  297. UNION ALL
  298. SELECT view_name AS table_name FROM user_views
  299. UNION ALL
  300. SELECT mview_name AS table_name FROM user_mviews
  301. ORDER BY table_name
  302. SQL;
  303. $command = $this->db->createCommand($sql);
  304. } else {
  305. $sql = <<<SQL
  306. SELECT object_name AS table_name
  307. FROM all_objects
  308. WHERE object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') AND owner=:schema
  309. ORDER BY object_name
  310. SQL;
  311. $command = $this->db->createCommand($sql, [':schema' => $schema]);
  312. }
  313. $rows = $command->queryAll();
  314. $names = [];
  315. foreach ($rows as $row) {
  316. if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) {
  317. $row = array_change_key_case($row, CASE_UPPER);
  318. }
  319. $names[] = $row['TABLE_NAME'];
  320. }
  321. return $names;
  322. }
  323. /**
  324. * Returns all unique indexes for the given table.
  325. * Each array element is of the following structure:
  326. *
  327. * ```php
  328. * [
  329. * 'IndexName1' => ['col1' [, ...]],
  330. * 'IndexName2' => ['col2' [, ...]],
  331. * ]
  332. * ```
  333. *
  334. * @param TableSchema $table the table metadata
  335. * @return array all unique indexes for the given table.
  336. * @since 2.0.4
  337. */
  338. public function findUniqueIndexes($table)
  339. {
  340. $query = <<<SQL
  341. SELECT dic.INDEX_NAME, dic.COLUMN_NAME
  342. FROM ALL_INDEXES di
  343. INNER JOIN ALL_IND_COLUMNS dic ON di.TABLE_NAME = dic.TABLE_NAME AND di.INDEX_NAME = dic.INDEX_NAME
  344. WHERE di.UNIQUENESS = 'UNIQUE'
  345. AND dic.TABLE_OWNER = :schemaName
  346. AND dic.TABLE_NAME = :tableName
  347. ORDER BY dic.TABLE_NAME, dic.INDEX_NAME, dic.COLUMN_POSITION
  348. SQL;
  349. $result = [];
  350. $command = $this->db->createCommand($query, [
  351. ':tableName' => $table->name,
  352. ':schemaName' => $table->schemaName,
  353. ]);
  354. foreach ($command->queryAll() as $row) {
  355. $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
  356. }
  357. return $result;
  358. }
  359. /**
  360. * Extracts the data types for the given column
  361. * @param ColumnSchema $column
  362. * @param string $dbType DB type
  363. * @param string $precision total number of digits.
  364. * This parameter is available since version 2.0.4.
  365. * @param string $scale number of digits on the right of the decimal separator.
  366. * This parameter is available since version 2.0.4.
  367. * @param string $length length for character types.
  368. * This parameter is available since version 2.0.4.
  369. */
  370. protected function extractColumnType($column, $dbType, $precision, $scale, $length)
  371. {
  372. $column->dbType = $dbType;
  373. if (strpos($dbType, 'FLOAT') !== false || strpos($dbType, 'DOUBLE') !== false) {
  374. $column->type = 'double';
  375. } elseif (strpos($dbType, 'NUMBER') !== false) {
  376. if ($scale === null || $scale > 0) {
  377. $column->type = 'decimal';
  378. } else {
  379. $column->type = 'integer';
  380. }
  381. } elseif (strpos($dbType, 'INTEGER') !== false) {
  382. $column->type = 'integer';
  383. } elseif (strpos($dbType, 'BLOB') !== false) {
  384. $column->type = 'binary';
  385. } elseif (strpos($dbType, 'CLOB') !== false) {
  386. $column->type = 'text';
  387. } elseif (strpos($dbType, 'TIMESTAMP') !== false) {
  388. $column->type = 'timestamp';
  389. } else {
  390. $column->type = 'string';
  391. }
  392. }
  393. /**
  394. * Extracts size, precision and scale information from column's DB type.
  395. * @param ColumnSchema $column
  396. * @param string $dbType the column's DB type
  397. * @param string $precision total number of digits.
  398. * This parameter is available since version 2.0.4.
  399. * @param string $scale number of digits on the right of the decimal separator.
  400. * This parameter is available since version 2.0.4.
  401. * @param string $length length for character types.
  402. * This parameter is available since version 2.0.4.
  403. */
  404. protected function extractColumnSize($column, $dbType, $precision, $scale, $length)
  405. {
  406. $column->size = trim($length) === '' ? null : (int)$length;
  407. $column->precision = trim($precision) === '' ? null : (int)$precision;
  408. $column->scale = trim($scale) === '' ? null : (int)$scale;
  409. }
  410. /**
  411. * @inheritdoc
  412. */
  413. public function insert($table, $columns)
  414. {
  415. $params = [];
  416. $returnParams = [];
  417. $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
  418. $tableSchema = $this->getTableSchema($table);
  419. $returnColumns = $tableSchema->primaryKey;
  420. if (!empty($returnColumns)) {
  421. $columnSchemas = $tableSchema->columns;
  422. $returning = [];
  423. foreach ((array)$returnColumns as $name) {
  424. $phName = QueryBuilder::PARAM_PREFIX . (count($params) + count($returnParams));
  425. $returnParams[$phName] = [
  426. 'column' => $name,
  427. 'value' => null,
  428. ];
  429. if (!isset($columnSchemas[$name]) || $columnSchemas[$name]->phpType !== 'integer') {
  430. $returnParams[$phName]['dataType'] = \PDO::PARAM_STR;
  431. } else {
  432. $returnParams[$phName]['dataType'] = \PDO::PARAM_INT;
  433. }
  434. $returnParams[$phName]['size'] = isset($columnSchemas[$name]) && isset($columnSchemas[$name]->size) ? $columnSchemas[$name]->size : -1;
  435. $returning[] = $this->quoteColumnName($name);
  436. }
  437. $sql .= ' RETURNING ' . implode(', ', $returning) . ' INTO ' . implode(', ', array_keys($returnParams));
  438. }
  439. $command = $this->db->createCommand($sql, $params);
  440. $command->prepare(false);
  441. foreach ($returnParams as $name => &$value) {
  442. $command->pdoStatement->bindParam($name, $value['value'], $value['dataType'], $value['size']);
  443. }
  444. if (!$command->execute()) {
  445. return false;
  446. }
  447. $result = [];
  448. foreach ($returnParams as $value) {
  449. $result[$value['column']] = $value['value'];
  450. }
  451. return $result;
  452. }
  453. }