* @since 2.0 */ class Schema extends \yii\db\Schema { /** * @var array map of DB errors and corresponding exceptions * If left part is found in DB error message exception class from the right part is used. */ public $exceptionMap = [ 'ORA-00001: unique constraint' => 'yii\db\IntegrityException', ]; /** * @inheritdoc */ public function init() { parent::init(); if ($this->defaultSchema === null) { $this->defaultSchema = strtoupper($this->db->username); } } /** * @inheritdoc */ public function releaseSavepoint($name) { // does nothing as Oracle does not support this } /** * @inheritdoc */ public function quoteSimpleTableName($name) { return strpos($name, '"') !== false ? $name : '"' . $name . '"'; } /** * @inheritdoc */ public function createQueryBuilder() { return new QueryBuilder($this->db); } /** * @inheritdoc */ public function loadTableSchema($name) { $table = new TableSchema(); $this->resolveTableNames($table, $name); if ($this->findColumns($table)) { $this->findConstraints($table); return $table; } else { return null; } } /** * Resolves the table name and schema name (if any). * * @param TableSchema $table the table metadata object * @param string $name the table name */ protected function resolveTableNames($table, $name) { $parts = explode('.', str_replace('"', '', $name)); if (isset($parts[1])) { $table->schemaName = $parts[0]; $table->name = $parts[1]; } else { $table->schemaName = $this->defaultSchema; $table->name = $name; } $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; } /** * Collects the table column metadata. * @param TableSchema $table the table schema * @return boolean whether the table exists */ protected function findColumns($table) { $sql = <<db->createCommand($sql, [ ':tableName' => $table->name, ':schemaName' => $table->schemaName, ])->queryAll(); } catch (\Exception $e) { return false; } if (empty($columns)) { return false; } foreach ($columns as $column) { if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) { $column = array_change_key_case($column, CASE_UPPER); } $c = $this->createColumn($column); $table->columns[$c->name] = $c; if ($c->isPrimaryKey) { $table->primaryKey[] = $c->name; $table->sequenceName = $this->getTableSequenceName($table->name); } } return true; } /** * Sequence name of table * * @param $tableName * @internal param \yii\db\TableSchema $table->name the table schema * @return string whether the sequence exists */ protected function getTableSequenceName($tableName) { $seq_name_sql = <<db->createCommand($seq_name_sql, [':tableName' => $tableName])->queryScalar(); return $sequenceName === false ? null : $sequenceName; } /** * @Overrides method in class 'Schema' * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php -> Oracle does not support this * * Returns the ID of the last inserted row or sequence value. * @param string $sequenceName name of the sequence object (required by some DBMS) * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object * @throws InvalidCallException if the DB connection is not active */ public function getLastInsertID($sequenceName = '') { if ($this->db->isActive) { // get the last insert id from the master connection $sequenceName = $this->quoteSimpleTableName($sequenceName); return $this->db->useMaster(function (Connection $db) use ($sequenceName) { return $db->createCommand("SELECT {$sequenceName}.CURRVAL FROM DUAL")->queryScalar(); }); } else { throw new InvalidCallException('DB Connection is not active.'); } } /** * Creates ColumnSchema instance * * @param array $column * @return ColumnSchema */ protected function createColumn($column) { $c = $this->createColumnSchema(); $c->name = $column['COLUMN_NAME']; $c->allowNull = $column['NULLABLE'] === 'Y'; $c->isPrimaryKey = strpos($column['KEY'], 'P') !== false; $c->comment = $column['COLUMN_COMMENT'] === null ? '' : $column['COLUMN_COMMENT']; $this->extractColumnType($c, $column['DATA_TYPE'], $column['DATA_PRECISION'], $column['DATA_SCALE'], $column['DATA_LENGTH']); $this->extractColumnSize($c, $column['DATA_TYPE'], $column['DATA_PRECISION'], $column['DATA_SCALE'], $column['DATA_LENGTH']); $c->phpType = $this->getColumnPhpType($c); if (!$c->isPrimaryKey) { if (stripos($column['DATA_DEFAULT'], 'timestamp') !== false) { $c->defaultValue = null; } else { $defaultValue = $column['DATA_DEFAULT']; if ($c->type === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') { $c->defaultValue = new Expression('CURRENT_TIMESTAMP'); } else { if ($defaultValue !== null) { if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'" && $defaultValue[$len - 1] === "'" ) { $defaultValue = substr($column['DATA_DEFAULT'], 1, -1); } else { $defaultValue = trim($defaultValue); } } $c->defaultValue = $c->phpTypecast($defaultValue); } } } return $c; } /** * Finds constraints and fills them into TableSchema object passed * @param TableSchema $table */ protected function findConstraints($table) { $sql = <<db->createCommand($sql, [ ':tableName' => $table->name, ':schemaName' => $table->schemaName, ]); $constraints = []; foreach ($command->queryAll() as $row) { if ($row['CONSTRAINT_TYPE'] !== 'R') { // this condition is not checked in SQL WHERE because of an Oracle Bug: // see https://github.com/yiisoft/yii2/pull/8844 continue; } if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) { $row = array_change_key_case($row, CASE_UPPER); } $name = $row['CONSTRAINT_NAME']; if (!isset($constraints[$name])) { $constraints[$name] = [ 'tableName' => $row["TABLE_REF"], 'columns' => [], ]; } $constraints[$name]['columns'][$row["COLUMN_NAME"]] = $row["COLUMN_REF"]; } foreach ($constraints as $constraint) { $table->foreignKeys[] = array_merge([$constraint['tableName']], $constraint['columns']); } } /** * @inheritdoc */ protected function findSchemaNames() { $sql = <<db->createCommand($sql)->queryColumn(); } /** * @inheritdoc */ protected function findTableNames($schema = '') { if ($schema === '') { $sql = <<db->createCommand($sql); } else { $sql = <<db->createCommand($sql, [':schema' => $schema]); } $rows = $command->queryAll(); $names = []; foreach ($rows as $row) { if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_LOWER) { $row = array_change_key_case($row, CASE_UPPER); } $names[] = $row['TABLE_NAME']; } return $names; } /** * Returns all unique indexes for the given table. * Each array element is of the following structure: * * ~~~ * [ * 'IndexName1' => ['col1' [, ...]], * 'IndexName2' => ['col2' [, ...]], * ] * ~~~ * * @param TableSchema $table the table metadata * @return array all unique indexes for the given table. * @since 2.0.4 */ public function findUniqueIndexes($table) { $query = <<db->createCommand($query, [ ':tableName' => $table->name, ':schemaName' => $table->schemaName, ]); foreach ($command->queryAll() as $row) { $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME']; } return $result; } /** * Extracts the data types for the given column * @param ColumnSchema $column * @param string $dbType DB type * @param string $precision total number of digits. * This parameter is available since version 2.0.4. * @param string $scale number of digits on the right of the decimal separator. * This parameter is available since version 2.0.4. * @param string $length length for character types. * This parameter is available since version 2.0.4. */ protected function extractColumnType($column, $dbType, $precision, $scale, $length) { $column->dbType = $dbType; if (strpos($dbType, 'FLOAT') !== false || strpos($dbType, 'DOUBLE') !== false) { $column->type = 'double'; } elseif ($dbType == 'NUMBER' || strpos($dbType, 'INTEGER') !== false) { if ($scale !== null && $scale > 0) { $column->type = 'decimal'; } else { $column->type = 'integer'; } } elseif (strpos($dbType, 'BLOB') !== false) { $column->type = 'binary'; } elseif (strpos($dbType, 'CLOB') !== false) { $column->type = 'text'; } elseif (strpos($dbType, 'TIMESTAMP') !== false) { $column->type = 'timestamp'; } else { $column->type = 'string'; } } /** * Extracts size, precision and scale information from column's DB type. * @param ColumnSchema $column * @param string $dbType the column's DB type * @param string $precision total number of digits. * This parameter is available since version 2.0.4. * @param string $scale number of digits on the right of the decimal separator. * This parameter is available since version 2.0.4. * @param string $length length for character types. * This parameter is available since version 2.0.4. */ protected function extractColumnSize($column, $dbType, $precision, $scale, $length) { $column->size = trim($length) == '' ? null : (int) $length; $column->precision = trim($precision) == '' ? null : (int) $precision; $column->scale = trim($scale) == '' ? null : (int) $scale; } /** * @inheritdoc */ public function insert($table, $columns) { $params = []; $returnParams = []; $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params); $tableSchema = $this->getTableSchema($table); $returnColumns = $tableSchema->primaryKey; if (!empty($returnColumns)) { $columnSchemas = $tableSchema->columns; $returning = []; foreach ((array) $returnColumns as $name) { $phName = QueryBuilder::PARAM_PREFIX . (count($params) + count($returnParams)); $returnParams[$phName] = [ 'column' => $name, 'value' => null, ]; if (!isset($columnSchemas[$name]) || $columnSchemas[$name]->phpType !== 'integer') { $returnParams[$phName]['dataType'] = \PDO::PARAM_STR; } else { $returnParams[$phName]['dataType'] = \PDO::PARAM_INT; } $returnParams[$phName]['size'] = isset($columnSchemas[$name]) && isset($columnSchemas[$name]->size) ? $columnSchemas[$name]->size : -1; $returning[] = $this->quoteColumnName($name); } $sql .= ' RETURNING ' . implode(', ', $returning) . ' INTO ' . implode(', ', array_keys($returnParams)); } $command = $this->db->createCommand($sql, $params); $command->prepare(false); foreach ($returnParams as $name => &$value) { $command->pdoStatement->bindParam($name, $value['value'], $value['dataType'], $value['size']); } if (!$command->execute()) { return false; } $result = []; foreach ($returnParams as $value) { $result[$value['column']] = $value['value']; } return $result; } }