= 10.0) * * @var bool */ protected $useIdentity; /** * {@inheritDoc} * * @throws \RuntimeException * @throws \InvalidArgumentException * @return void */ public function connect(): void { if ($this->connection === null) { if (!class_exists('PDO') || !in_array('pgsql', PDO::getAvailableDrivers(), true)) { // @codeCoverageIgnoreStart throw new RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.'); // @codeCoverageIgnoreEnd } $options = $this->getOptions(); $dsn = 'pgsql:dbname=' . $options['name']; if (isset($options['host'])) { $dsn .= ';host=' . $options['host']; } // if custom port is specified use it if (isset($options['port'])) { $dsn .= ';port=' . $options['port']; } $driverOptions = []; // use custom data fetch mode if (!empty($options['fetch_mode'])) { $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); } // pass \PDO::ATTR_PERSISTENT to driver options instead of useless setting it after instantiation if (isset($options['attr_persistent'])) { $driverOptions[PDO::ATTR_PERSISTENT] = $options['attr_persistent']; } $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions); try { if (isset($options['schema'])) { $db->exec('SET search_path TO ' . $this->quoteSchemaName($options['schema'])); } } catch (PDOException $exception) { throw new InvalidArgumentException( sprintf('Schema does not exists: %s', $options['schema']), 0, $exception ); } $this->useIdentity = (float)$db->getAttribute(PDO::ATTR_SERVER_VERSION) >= 10; $this->setConnection($db); } } /** * @inheritDoc */ public function disconnect(): void { $this->connection = null; } /** * @inheritDoc */ public function hasTransactions(): bool { return true; } /** * @inheritDoc */ public function beginTransaction(): void { $this->execute('BEGIN'); } /** * @inheritDoc */ public function commitTransaction(): void { $this->execute('COMMIT'); } /** * @inheritDoc */ public function rollbackTransaction(): void { $this->execute('ROLLBACK'); } /** * Quotes a schema name for use in a query. * * @param string $schemaName Schema Name * @return string */ public function quoteSchemaName(string $schemaName): string { return $this->quoteColumnName($schemaName); } /** * @inheritDoc */ public function quoteTableName(string $tableName): string { $parts = $this->getSchemaName($tableName); return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']); } /** * @inheritDoc */ public function quoteColumnName(string $columnName): string { return '"' . $columnName . '"'; } /** * @inheritDoc */ public function hasTable(string $tableName): bool { if ($this->hasCreatedTable($tableName)) { return true; } $parts = $this->getSchemaName($tableName); $result = $this->getConnection()->query( sprintf( 'SELECT * FROM information_schema.tables WHERE table_schema = %s AND table_name = %s', $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']) ) ); return $result->rowCount() === 1; } /** * @inheritDoc */ public function createTable(Table $table, array $columns = [], array $indexes = []): void { $queries = []; $options = $table->getOptions(); $parts = $this->getSchemaName($table->getName()); // Add the default primary key if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { $options['id'] = 'id'; } if (isset($options['id']) && is_string($options['id'])) { // Handle id => "field_name" to support AUTO_INCREMENT $column = new Column(); $column->setName($options['id']) ->setType('integer') ->setOptions(['identity' => true]); array_unshift($columns, $column); if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) { throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key'); } $options['primary_key'] = $options['id']; } // TODO - process table options like collation etc $sql = 'CREATE TABLE '; $sql .= $this->quoteTableName($table->getName()) . ' ('; $this->columnsWithComments = []; foreach ($columns as $column) { $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column); if ($this->useIdentity && $column->getIdentity() && $column->getGenerated() !== null) { $sql .= sprintf(' GENERATED %s AS IDENTITY', $column->getGenerated()); } $sql .= ', '; // set column comments, if needed if ($column->getComment()) { $this->columnsWithComments[] = $column; } } // set the primary key(s) if (isset($options['primary_key'])) { $sql = rtrim($sql); $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey')); if (is_string($options['primary_key'])) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($options['primary_key']); } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); } $sql .= ')'; } else { $sql = rtrim($sql, ', '); // no primary keys } $sql .= ')'; $queries[] = $sql; // process column comments if (!empty($this->columnsWithComments)) { foreach ($this->columnsWithComments as $column) { $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName()); } } // set the indexes if (!empty($indexes)) { foreach ($indexes as $index) { $queries[] = $this->getIndexSqlDefinition($index, $table->getName()); } } // process table comments if (isset($options['comment'])) { $queries[] = sprintf( 'COMMENT ON TABLE %s IS %s', $this->quoteTableName($table->getName()), $this->getConnection()->quote($options['comment']) ); } foreach ($queries as $query) { $this->execute($query); } $this->addCreatedTable($table->getName()); } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getChangePrimaryKeyInstructions(Table $table, $newColumns): AlterInstructions { $parts = $this->getSchemaName($table->getName()); $instructions = new AlterInstructions(); // Drop the existing primary key $primaryKey = $this->getPrimaryKey($table->getName()); if (!empty($primaryKey['constraint'])) { $sql = sprintf( 'DROP CONSTRAINT %s', $this->quoteColumnName($primaryKey['constraint']) ); $instructions->addAlter($sql); } // Add the new primary key if (!empty($newColumns)) { $sql = sprintf( 'ADD CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey') ); if (is_string($newColumns)) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($newColumns); } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id') $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); } else { throw new InvalidArgumentException(sprintf( 'Invalid value for primary key: %s', json_encode($newColumns) )); } $sql .= ')'; $instructions->addAlter($sql); } return $instructions; } /** * @inheritDoc */ protected function getChangeCommentInstructions(Table $table, ?string $newComment): AlterInstructions { $instructions = new AlterInstructions(); // passing 'null' is to remove table comment $newComment = $newComment !== null ? $this->getConnection()->quote($newComment) : 'NULL'; $sql = sprintf( 'COMMENT ON TABLE %s IS %s', $this->quoteTableName($table->getName()), $newComment ); $instructions->addPostStep($sql); return $instructions; } /** * @inheritDoc */ protected function getRenameTableInstructions(string $tableName, string $newTableName): AlterInstructions { $this->updateCreatedTableName($tableName, $newTableName); $sql = sprintf( 'ALTER TABLE %s RENAME TO %s', $this->quoteTableName($tableName), $this->quoteColumnName($newTableName) ); return new AlterInstructions([], [$sql]); } /** * @inheritDoc */ protected function getDropTableInstructions(string $tableName): AlterInstructions { $this->removeCreatedTable($tableName); $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); return new AlterInstructions([], [$sql]); } /** * @inheritDoc */ public function truncateTable(string $tableName): void { $sql = sprintf( 'TRUNCATE TABLE %s RESTART IDENTITY', $this->quoteTableName($tableName) ); $this->execute($sql); } /** * @inheritDoc */ public function getColumns(string $tableName): array { $parts = $this->getSchemaName($tableName); $columns = []; $sql = sprintf( 'SELECT column_name, data_type, udt_name, is_identity, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale, datetime_precision %s FROM information_schema.columns WHERE table_schema = %s AND table_name = %s ORDER BY ordinal_position', $this->useIdentity ? ', identity_generation' : '', $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']) ); $columnsInfo = $this->fetchAll($sql); foreach ($columnsInfo as $columnInfo) { $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED'; if ($isUserDefined) { $columnType = Literal::from($columnInfo['udt_name']); } else { $columnType = $this->getPhinxType($columnInfo['data_type']); } // If the default value begins with a ' or looks like a function mark it as literal if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") { if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) { // '' and \' are replaced with a single ' $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]); } else { $columnDefault = Literal::from($columnInfo['column_default']); } } elseif ( $columnInfo['column_default'] !== null && preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default']) ) { $columnDefault = Literal::from($columnInfo['column_default']); } else { $columnDefault = $columnInfo['column_default']; } $column = new Column(); $column->setName($columnInfo['column_name']) ->setType($columnType) ->setNull($columnInfo['is_nullable'] === 'YES') ->setDefault($columnDefault) ->setIdentity($columnInfo['is_identity'] === 'YES') ->setScale($columnInfo['numeric_scale']); if ($this->useIdentity) { $column->setGenerated($columnInfo['identity_generation']); } if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) { $column->setTimezone(true); } if (isset($columnInfo['character_maximum_length'])) { $column->setLimit($columnInfo['character_maximum_length']); } if (in_array($columnType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME], true)) { $column->setPrecision($columnInfo['datetime_precision']); } elseif ( !in_array($columnType, [ self::PHINX_TYPE_SMALL_INTEGER, self::PHINX_TYPE_INTEGER, self::PHINX_TYPE_BIG_INTEGER, ], true) ) { $column->setPrecision($columnInfo['numeric_precision']); } $columns[] = $column; } return $columns; } /** * @inheritDoc */ public function hasColumn(string $tableName, string $columnName): bool { $parts = $this->getSchemaName($tableName); $sql = sprintf( 'SELECT count(*) FROM information_schema.columns WHERE table_schema = %s AND table_name = %s AND column_name = %s', $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']), $this->getConnection()->quote($columnName) ); $result = $this->fetchRow($sql); return $result['count'] > 0; } /** * @inheritDoc */ protected function getAddColumnInstructions(Table $table, Column $column): AlterInstructions { $instructions = new AlterInstructions(); $instructions->addAlter(sprintf( 'ADD %s %s %s', $this->quoteColumnName($column->getName()), $this->getColumnSqlDefinition($column), $column->isIdentity() && $column->getGenerated() !== null && $this->useIdentity ? sprintf('GENERATED %s AS IDENTITY', $column->getGenerated()) : '' )); if ($column->getComment()) { $instructions->addPostStep($this->getColumnCommentSqlDefinition($column, $table->getName())); } return $instructions; } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getRenameColumnInstructions( string $tableName, string $columnName, string $newColumnName ): AlterInstructions { $parts = $this->getSchemaName($tableName); $sql = sprintf( 'SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists FROM information_schema.columns WHERE table_schema = %s AND table_name = %s AND column_name = %s', $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']), $this->getConnection()->quote($columnName) ); $result = $this->fetchRow($sql); if (!(bool)$result['column_exists']) { throw new InvalidArgumentException("The specified column does not exist: $columnName"); } $instructions = new AlterInstructions(); $instructions->addPostStep( sprintf( 'ALTER TABLE %s RENAME COLUMN %s TO %s', $this->quoteTableName($tableName), $this->quoteColumnName($columnName), $this->quoteColumnName($newColumnName) ) ); return $instructions; } /** * @inheritDoc */ protected function getChangeColumnInstructions( string $tableName, string $columnName, Column $newColumn ): AlterInstructions { $quotedColumnName = $this->quoteColumnName($columnName); $instructions = new AlterInstructions(); if ($newColumn->getType() === 'boolean') { $sql = sprintf('ALTER COLUMN %s DROP DEFAULT', $quotedColumnName); $instructions->addAlter($sql); } $sql = sprintf( 'ALTER COLUMN %s TYPE %s', $quotedColumnName, $this->getColumnSqlDefinition($newColumn) ); if (in_array($newColumn->getType(), ['smallinteger', 'integer', 'biginteger'], true)) { $sql .= sprintf( ' USING (%s::bigint)', $quotedColumnName ); } if ($newColumn->getType() === 'uuid') { $sql .= sprintf( ' USING (%s::uuid)', $quotedColumnName ); } //NULL and DEFAULT cannot be set while changing column type $sql = preg_replace('/ NOT NULL/', '', $sql); $sql = preg_replace('/ NULL/', '', $sql); //If it is set, DEFAULT is the last definition $sql = preg_replace('/DEFAULT .*/', '', $sql); if ($newColumn->getType() === 'boolean') { $sql .= sprintf( ' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)', $quotedColumnName, $quotedColumnName ); } $instructions->addAlter($sql); $column = $this->getColumn($tableName, $columnName); if ($this->useIdentity) { // process identity $sql = sprintf( 'ALTER COLUMN %s', $quotedColumnName ); if ($newColumn->isIdentity() && $newColumn->getGenerated() !== null) { if ($column->isIdentity()) { $sql .= sprintf(' SET GENERATED %s', $newColumn->getGenerated()); } else { $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', $newColumn->getGenerated()); } } else { $sql .= ' DROP IDENTITY IF EXISTS'; } $instructions->addAlter($sql); } // process null $sql = sprintf( 'ALTER COLUMN %s', $quotedColumnName ); if (!$newColumn->getIdentity() && !$column->getIdentity() && $newColumn->isNull()) { $sql .= ' DROP NOT NULL'; } else { $sql .= ' SET NOT NULL'; } $instructions->addAlter($sql); if ($newColumn->getDefault() !== null) { $instructions->addAlter(sprintf( 'ALTER COLUMN %s SET %s', $quotedColumnName, $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType()) )); } elseif (!$newColumn->getIdentity()) { //drop default $instructions->addAlter(sprintf( 'ALTER COLUMN %s DROP DEFAULT', $quotedColumnName )); } // rename column if ($columnName !== $newColumn->getName()) { $instructions->addPostStep(sprintf( 'ALTER TABLE %s RENAME COLUMN %s TO %s', $this->quoteTableName($tableName), $quotedColumnName, $this->quoteColumnName($newColumn->getName()) )); } // change column comment if needed if ($newColumn->getComment()) { $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName)); } return $instructions; } /** * @param string $tableName Table name * @param string $columnName Column name * @return ?\Phinx\Db\Table\Column */ protected function getColumn(string $tableName, string $columnName): ?Column { $columns = $this->getColumns($tableName); foreach ($columns as $column) { if ($column->getName() === $columnName) { return $column; } } return null; } /** * @inheritDoc */ protected function getDropColumnInstructions(string $tableName, string $columnName): AlterInstructions { $alter = sprintf( 'DROP COLUMN %s', $this->quoteColumnName($columnName) ); return new AlterInstructions([$alter]); } /** * Get an array of indexes from a particular table. * * @param string $tableName Table name * @return array */ protected function getIndexes($tableName) { $parts = $this->getSchemaName($tableName); $indexes = []; $sql = sprintf( "SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_namespace nsp WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relnamespace = nsp.oid AND nsp.nspname = %s AND t.relkind = 'r' AND t.relname = %s ORDER BY t.relname, i.relname", $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']) ); $rows = $this->fetchAll($sql); foreach ($rows as $row) { if (!isset($indexes[$row['index_name']])) { $indexes[$row['index_name']] = ['columns' => []]; } $indexes[$row['index_name']]['columns'][] = $row['column_name']; } return $indexes; } /** * @inheritDoc */ public function hasIndex(string $tableName, $columns): bool { if (is_string($columns)) { $columns = [$columns]; } $indexes = $this->getIndexes($tableName); foreach ($indexes as $index) { if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) { return true; } } return false; } /** * @inheritDoc */ public function hasIndexByName(string $tableName, string $indexName): bool { $indexes = $this->getIndexes($tableName); foreach ($indexes as $name => $index) { if ($name === $indexName) { return true; } } return false; } /** * @inheritDoc */ protected function getAddIndexInstructions(Table $table, Index $index): AlterInstructions { $instructions = new AlterInstructions(); $instructions->addPostStep($this->getIndexSqlDefinition($index, $table->getName())); return $instructions; } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getDropIndexByColumnsInstructions(string $tableName, $columns): AlterInstructions { $parts = $this->getSchemaName($tableName); if (is_string($columns)) { $columns = [$columns]; // str to array } $indexes = $this->getIndexes($tableName); foreach ($indexes as $indexName => $index) { $a = array_diff($columns, $index['columns']); if (empty($a)) { return new AlterInstructions([], [sprintf( 'DROP INDEX IF EXISTS %s', '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName)) )]); } } throw new InvalidArgumentException(sprintf( "The specified index on columns '%s' does not exist", implode(',', $columns) )); } /** * @inheritDoc */ protected function getDropIndexByNameInstructions(string $tableName, string $indexName): AlterInstructions { $parts = $this->getSchemaName($tableName); $sql = sprintf( 'DROP INDEX IF EXISTS %s', '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName)) ); return new AlterInstructions([], [$sql]); } /** * @inheritDoc */ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint = null): bool { $primaryKey = $this->getPrimaryKey($tableName); if (empty($primaryKey)) { return false; } if ($constraint) { return $primaryKey['constraint'] === $constraint; } else { if (is_string($columns)) { $columns = [$columns]; // str to array } $missingColumns = array_diff($columns, $primaryKey['columns']); return empty($missingColumns); } } /** * Get the primary key from a particular table. * * @param string $tableName Table name * @return array */ public function getPrimaryKey(string $tableName): array { $parts = $this->getSchemaName($tableName); $rows = $this->fetchAll(sprintf( "SELECT tc.constraint_name, kcu.column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name WHERE constraint_type = 'PRIMARY KEY' AND tc.table_schema = %s AND tc.table_name = %s ORDER BY kcu.position_in_unique_constraint", $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']) )); $primaryKey = [ 'columns' => [], ]; foreach ($rows as $row) { $primaryKey['constraint'] = $row['constraint_name']; $primaryKey['columns'][] = $row['column_name']; } return $primaryKey; } /** * @inheritDoc */ public function hasForeignKey(string $tableName, $columns, ?string $constraint = null): bool { if (is_string($columns)) { $columns = [$columns]; // str to array } $foreignKeys = $this->getForeignKeys($tableName); if ($constraint) { if (isset($foreignKeys[$constraint])) { return !empty($foreignKeys[$constraint]); } return false; } foreach ($foreignKeys as $key) { $a = array_diff($columns, $key['columns']); if (empty($a)) { return true; } } return false; } /** * Get an array of foreign keys from a particular table. * * @param string $tableName Table name * @return array */ protected function getForeignKeys(string $tableName): array { $parts = $this->getSchemaName($tableName); $foreignKeys = []; $rows = $this->fetchAll(sprintf( "SELECT tc.constraint_name, tc.table_name, kcu.column_name, ccu.table_name AS referenced_table_name, ccu.column_name AS referenced_column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s ORDER BY kcu.position_in_unique_constraint", $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']) )); foreach ($rows as $row) { $foreignKeys[$row['constraint_name']]['table'] = $row['table_name']; $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name']; $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name']; $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name']; } return $foreignKeys; } /** * @inheritDoc */ protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey): AlterInstructions { $alter = sprintf( 'ADD %s', $this->getForeignKeySqlDefinition($foreignKey, $table->getName()) ); return new AlterInstructions([$alter]); } /** * @inheritDoc */ protected function getDropForeignKeyInstructions($tableName, $constraint): AlterInstructions { $alter = sprintf( 'DROP CONSTRAINT %s', $this->quoteColumnName($constraint) ); return new AlterInstructions([$alter]); } /** * @inheritDoc */ protected function getDropForeignKeyByColumnsInstructions(string $tableName, array $columns): AlterInstructions { $instructions = new AlterInstructions(); $parts = $this->getSchemaName($tableName); $sql = 'SELECT c.CONSTRAINT_NAME FROM ( SELECT CONSTRAINT_NAME, array_agg(COLUMN_NAME::varchar) as columns FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = %s AND TABLE_NAME IS NOT NULL AND TABLE_NAME = %s AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL GROUP BY CONSTRAINT_NAME ) c WHERE ARRAY[%s]::varchar[] <@ c.columns AND ARRAY[%s]::varchar[] @> c.columns'; $array = []; foreach ($columns as $col) { $array[] = "'$col'"; } $rows = $this->fetchAll(sprintf( $sql, $this->getConnection()->quote($parts['schema']), $this->getConnection()->quote($parts['table']), implode(',', $array), implode(',', $array) )); foreach ($rows as $row) { $newInstr = $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']); $instructions->merge($newInstr); } return $instructions; } /** * {@inheritDoc} * * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException */ public function getSqlType($type, ?int $limit = null): array { switch ($type) { case static::PHINX_TYPE_TEXT: case static::PHINX_TYPE_TIME: case static::PHINX_TYPE_DATE: case static::PHINX_TYPE_BOOLEAN: case static::PHINX_TYPE_JSON: case static::PHINX_TYPE_JSONB: case static::PHINX_TYPE_UUID: case static::PHINX_TYPE_CIDR: case static::PHINX_TYPE_INET: case static::PHINX_TYPE_MACADDR: case static::PHINX_TYPE_TIMESTAMP: case static::PHINX_TYPE_INTEGER: return ['name' => $type]; case static::PHINX_TYPE_TINY_INTEGER: return ['name' => 'smallint']; case static::PHINX_TYPE_SMALL_INTEGER: return ['name' => 'smallint']; case static::PHINX_TYPE_DECIMAL: return ['name' => $type, 'precision' => 18, 'scale' => 0]; case static::PHINX_TYPE_DOUBLE: return ['name' => 'double precision']; case static::PHINX_TYPE_STRING: return ['name' => 'character varying', 'limit' => 255]; case static::PHINX_TYPE_CHAR: return ['name' => 'character', 'limit' => 255]; case static::PHINX_TYPE_BIG_INTEGER: return ['name' => 'bigint']; case static::PHINX_TYPE_FLOAT: return ['name' => 'real']; case static::PHINX_TYPE_DATETIME: return ['name' => 'timestamp']; case static::PHINX_TYPE_BINARYUUID: return ['name' => 'uuid']; case static::PHINX_TYPE_BLOB: case static::PHINX_TYPE_BINARY: return ['name' => 'bytea']; case static::PHINX_TYPE_INTERVAL: return ['name' => 'interval']; // Geospatial database types // Spatial storage in Postgres is done via the PostGIS extension, // which enables the use of the "geography" type in combination // with SRID 4326. case static::PHINX_TYPE_GEOMETRY: return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326]; case static::PHINX_TYPE_POINT: return ['name' => 'geography', 'type' => 'point', 'srid' => 4326]; case static::PHINX_TYPE_LINESTRING: return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326]; case static::PHINX_TYPE_POLYGON: return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326]; default: if ($this->isArrayType($type)) { return ['name' => $type]; } // Return array type throw new UnsupportedColumnTypeException('Column type `' . $type . '` is not supported by Postgresql.'); } } /** * Returns Phinx type by SQL type * * @param string $sqlType SQL type * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException * @return string Phinx type */ public function getPhinxType(string $sqlType): string { switch ($sqlType) { case 'character varying': case 'varchar': return static::PHINX_TYPE_STRING; case 'character': case 'char': return static::PHINX_TYPE_CHAR; case 'text': return static::PHINX_TYPE_TEXT; case 'json': return static::PHINX_TYPE_JSON; case 'jsonb': return static::PHINX_TYPE_JSONB; case 'smallint': return static::PHINX_TYPE_SMALL_INTEGER; case 'int': case 'int4': case 'integer': return static::PHINX_TYPE_INTEGER; case 'decimal': case 'numeric': return static::PHINX_TYPE_DECIMAL; case 'bigint': case 'int8': return static::PHINX_TYPE_BIG_INTEGER; case 'real': case 'float4': return static::PHINX_TYPE_FLOAT; case 'double precision': return static::PHINX_TYPE_DOUBLE; case 'bytea': return static::PHINX_TYPE_BINARY; case 'interval': return static::PHINX_TYPE_INTERVAL; case 'time': case 'timetz': case 'time with time zone': case 'time without time zone': return static::PHINX_TYPE_TIME; case 'date': return static::PHINX_TYPE_DATE; case 'timestamp': case 'timestamptz': case 'timestamp with time zone': case 'timestamp without time zone': return static::PHINX_TYPE_DATETIME; case 'bool': case 'boolean': return static::PHINX_TYPE_BOOLEAN; case 'uuid': return static::PHINX_TYPE_UUID; case 'cidr': return static::PHINX_TYPE_CIDR; case 'inet': return static::PHINX_TYPE_INET; case 'macaddr': return static::PHINX_TYPE_MACADDR; default: throw new UnsupportedColumnTypeException( 'Column type `' . $sqlType . '` is not supported by Postgresql.' ); } } /** * @inheritDoc */ public function createDatabase(string $name, array $options = []): void { $charset = $options['charset'] ?? 'utf8'; $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset)); } /** * @inheritDoc */ public function hasDatabase(string $name): bool { $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name); $result = $this->fetchRow($sql); return $result['count'] > 0; } /** * @inheritDoc */ public function dropDatabase($name): void { $this->disconnect(); $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name)); $this->createdTables = []; $this->connect(); } /** * Gets the PostgreSQL Column Definition for a Column object. * * @param \Phinx\Db\Table\Column $column Column * @return string */ protected function getColumnSqlDefinition(Column $column): string { $buffer = []; if ($column->isIdentity() && (!$this->useIdentity || $column->getGenerated() === null)) { if ($column->getType() === 'smallinteger') { $buffer[] = 'SMALLSERIAL'; } elseif ($column->getType() === 'biginteger') { $buffer[] = 'BIGSERIAL'; } else { $buffer[] = 'SERIAL'; } } elseif ($column->getType() instanceof Literal) { $buffer[] = (string)$column->getType(); } else { $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); $buffer[] = strtoupper($sqlType['name']); // integers cant have limits in postgres if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && ($column->getPrecision() || $column->getScale())) { $buffer[] = sprintf( '(%s, %s)', $column->getPrecision() ?: $sqlType['precision'], $column->getScale() ?: $sqlType['scale'] ); } elseif ($sqlType['name'] === self::PHINX_TYPE_GEOMETRY) { // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326) $buffer[] = sprintf( '(%s,%s)', strtoupper($sqlType['type']), $column->getSrid() ?: $sqlType['srid'] ); } elseif (in_array($sqlType['name'], [self::PHINX_TYPE_TIME, self::PHINX_TYPE_TIMESTAMP], true)) { if (is_numeric($column->getPrecision())) { $buffer[] = sprintf('(%s)', $column->getPrecision()); } if ($column->isTimezone()) { $buffer[] = strtoupper('with time zone'); } } elseif ( !in_array($column->getType(), [ self::PHINX_TYPE_TINY_INTEGER, self::PHINX_TYPE_SMALL_INTEGER, self::PHINX_TYPE_INTEGER, self::PHINX_TYPE_BIG_INTEGER, self::PHINX_TYPE_BOOLEAN, self::PHINX_TYPE_TEXT, self::PHINX_TYPE_BINARY, ], true) ) { if ($column->getLimit() || isset($sqlType['limit'])) { $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']); } } } $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL'; if ($column->getDefault() !== null) { $buffer[] = $this->getDefaultValueDefinition($column->getDefault(), $column->getType()); } return implode(' ', $buffer); } /** * Gets the PostgreSQL Column Comment Definition for a column object. * * @param \Phinx\Db\Table\Column $column Column * @param string $tableName Table name * @return string */ protected function getColumnCommentSqlDefinition(Column $column, string $tableName): string { // passing 'null' is to remove column comment $comment = strcasecmp($column->getComment(), 'NULL') !== 0 ? $this->getConnection()->quote($column->getComment()) : 'NULL'; return sprintf( 'COMMENT ON COLUMN %s.%s IS %s;', $this->quoteTableName($tableName), $this->quoteColumnName($column->getName()), $comment ); } /** * Gets the PostgreSQL Index Definition for an Index object. * * @param \Phinx\Db\Table\Index $index Index * @param string $tableName Table name * @return string */ protected function getIndexSqlDefinition(Index $index, string $tableName): string { $parts = $this->getSchemaName($tableName); $columnNames = $index->getColumns(); if (is_string($index->getName())) { $indexName = $index->getName(); } else { $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames)); } $order = $index->getOrder() ?? []; $columnNames = array_map(function ($columnName) use ($order) { $ret = '"' . $columnName . '"'; if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; } return $ret; }, $columnNames); $includedColumns = $index->getInclude() ? sprintf('INCLUDE ("%s")', implode('","', $index->getInclude())) : ''; $createIndexSentence = 'CREATE %s INDEX %s ON %s '; if ($index->getType() === self::GIN_INDEX_TYPE) { $createIndexSentence .= ' USING ' . $index->getType() . '(%s) %s;'; } else { $createIndexSentence .= '(%s) %s;'; } return sprintf( $createIndexSentence, ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''), $this->quoteColumnName($indexName), $this->quoteTableName($tableName), implode(',', $columnNames), $includedColumns ); } /** * Gets the MySQL Foreign Key Definition for an ForeignKey object. * * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key * @param string $tableName Table name * @return string */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { $parts = $this->getSchemaName($tableName); $constraintName = $foreignKey->getConstraint() ?: ( $parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey' ); $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) . ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' . " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" . implode('", "', $foreignKey->getReferencedColumns()) . '")'; if ($foreignKey->getOnDelete()) { $def .= " ON DELETE {$foreignKey->getOnDelete()}"; } if ($foreignKey->getOnUpdate()) { $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; } return $def; } /** * @inheritDoc */ public function createSchemaTable(): void { // Create the public/custom schema if it doesn't already exist if ($this->hasSchema($this->getGlobalSchemaName()) === false) { $this->createSchema($this->getGlobalSchemaName()); } $this->setSearchPath(); parent::createSchemaTable(); } /** * @inheritDoc */ public function getVersions(): array { $this->setSearchPath(); return parent::getVersions(); } /** * @inheritDoc */ public function getVersionLog(): array { $this->setSearchPath(); return parent::getVersionLog(); } /** * Creates the specified schema. * * @param string $schemaName Schema Name * @return void */ public function createSchema(string $schemaName = 'public'): void { // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name" $sql = sprintf('CREATE SCHEMA IF NOT EXISTS %s', $this->quoteSchemaName($schemaName)); $this->execute($sql); } /** * Checks to see if a schema exists. * * @param string $schemaName Schema Name * @return bool */ public function hasSchema(string $schemaName): bool { $sql = sprintf( 'SELECT count(*) FROM pg_namespace WHERE nspname = %s', $this->getConnection()->quote($schemaName) ); $result = $this->fetchRow($sql); return $result['count'] > 0; } /** * Drops the specified schema table. * * @param string $schemaName Schema name * @return void */ public function dropSchema(string $schemaName): void { $sql = sprintf('DROP SCHEMA IF EXISTS %s CASCADE', $this->quoteSchemaName($schemaName)); $this->execute($sql); foreach ($this->createdTables as $idx => $createdTable) { if ($this->getSchemaName($createdTable)['schema'] === $this->quoteSchemaName($schemaName)) { unset($this->createdTables[$idx]); } } } /** * Drops all schemas. * * @return void */ public function dropAllSchemas(): void { foreach ($this->getAllSchemas() as $schema) { $this->dropSchema($schema); } } /** * Returns schemas. * * @return array */ public function getAllSchemas(): array { $sql = "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'"; $items = $this->fetchAll($sql); $schemaNames = []; foreach ($items as $item) { $schemaNames[] = $item['schema_name']; } return $schemaNames; } /** * @inheritDoc */ public function getColumnTypes(): array { return array_merge(parent::getColumnTypes(), static::$specificColumnTypes); } /** * @inheritDoc */ public function isValidColumnType(Column $column): bool { // If not a standard column type, maybe it is array type? return parent::isValidColumnType($column) || $this->isArrayType($column->getType()); } /** * Check if the given column is an array of a valid type. * * @param string|\Phinx\Util\Literal $columnType Column type * @return bool */ protected function isArrayType($columnType): bool { if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) { return false; } $baseType = $matches[1]; return in_array($baseType, $this->getColumnTypes(), true); } /** * @param string $tableName Table name * @return array */ protected function getSchemaName(string $tableName): array { $schema = $this->getGlobalSchemaName(); $table = $tableName; if (strpos($tableName, '.') !== false) { [$schema, $table] = explode('.', $tableName); } return [ 'schema' => $schema, 'table' => $table, ]; } /** * Gets the schema name. * * @return string */ protected function getGlobalSchemaName(): string { $options = $this->getOptions(); return empty($options['schema']) ? 'public' : $options['schema']; } /** * @inheritDoc */ public function castToBool($value) { return (bool)$value ? 'TRUE' : 'FALSE'; } /** * @inheritDoc */ public function getDecoratedConnection(): Connection { $options = $this->getOptions(); $options = [ 'username' => $options['user'] ?? null, 'password' => $options['pass'] ?? null, 'database' => $options['name'], 'quoteIdentifiers' => true, ] + $options; $driver = new PostgresDriver($options); $driver->setConnection($this->connection); return new Connection(['driver' => $driver] + $options); } /** * Sets search path of schemas to look through for a table * * @return void */ public function setSearchPath(): void { $this->execute( sprintf( 'SET search_path TO %s,"$user",public', $this->quoteSchemaName($this->getGlobalSchemaName()) ) ); } }