Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Phinx/Db/Adapter/AdapterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ interface AdapterInterface
self::PHINX_TYPE_POLYGON,
];

// only for mysql so far
// MySQL-specific types
public const PHINX_TYPE_MEDIUM_INTEGER = 'mediuminteger';
public const PHINX_TYPE_ENUM = 'enum';
public const PHINX_TYPE_SET = 'set';
public const PHINX_TYPE_YEAR = 'year';

// Supported by MySQL and PostgreSQL
public const PHINX_TYPE_ENUM = 'enum';

// only for postgresql so far
public const PHINX_TYPE_CIDR = 'cidr';
public const PHINX_TYPE_INET = 'inet';
Expand Down
147 changes: 140 additions & 7 deletions src/Phinx/Db/Adapter/PostgresAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class PostgresAdapter extends PdoAdapter
self::PHINX_TYPE_MACADDR,
self::PHINX_TYPE_INTERVAL,
self::PHINX_TYPE_BINARYUUID,
self::PHINX_TYPE_ENUM,
];

private const GIN_INDEX_TYPE = 'gin';
Expand Down Expand Up @@ -277,7 +278,7 @@ public function createTable(Table $table, array $columns = [], array $indexes =

$this->columnsWithComments = [];
foreach ($columns as $column) {
$sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
$sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column, $table->getName());
if ($this->useIdentity && $column->getIdentity() && $column->getGenerated() !== null) {
$sql .= sprintf(' GENERATED %s AS IDENTITY', $column->getGenerated());
}
Expand All @@ -304,6 +305,22 @@ public function createTable(Table $table, array $columns = [], array $indexes =
}

$sql .= ')';

// Emit CREATE TYPE ... AS ENUM statements before the CREATE TABLE statement
foreach ($columns as $column) {
if ($column->getType() === static::PHINX_TYPE_ENUM) {
$values = $column->getValues();
if (!empty($values)) {
$typeName = $this->getEnumTypeName($table->getName(), $column->getName());
$queries[] = sprintf(
'CREATE TYPE %s AS ENUM (%s)',
$this->quoteColumnName($typeName),
implode(', ', array_map(fn($v) => $this->getConnection()->quote($v), $values)),
);
}
}
}

$queries[] = $sql;

// process column comments
Expand Down Expand Up @@ -423,8 +440,20 @@ protected function getDropTableInstructions(string $tableName): AlterInstruction
{
$this->removeCreatedTable($tableName);
$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
$instructions = new AlterInstructions([], [$sql]);

// Drop any Phinx-managed enum types associated with this table's columns
foreach ($this->getColumns($tableName) as $column) {
$typeName = $this->getEnumTypeName($tableName, $column->getName());
if ($this->hasEnumType($typeName)) {
$instructions->addPostStep(sprintf(
'DROP TYPE %s',
$this->quoteColumnName($typeName),
));
}
}

return new AlterInstructions([], [$sql]);
return $instructions;
}

/**
Expand Down Expand Up @@ -462,9 +491,15 @@ public function getColumns(string $tableName): array
$columnsInfo = $this->fetchAll($sql);
foreach ($columnsInfo as $columnInfo) {
$isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
$enumValues = null;

if ($isUserDefined) {
$columnType = Literal::from($columnInfo['udt_name']);
$enumValues = $this->getEnumTypeValues($columnInfo['udt_name']);
if ($enumValues !== null) {
$columnType = static::PHINX_TYPE_ENUM;
} else {
$columnType = Literal::from($columnInfo['udt_name']);
}
} else {
$columnType = $this->getPhinxType($columnInfo['data_type']);
}
Expand Down Expand Up @@ -512,6 +547,11 @@ public function getColumns(string $tableName): array
} elseif ($columnType === self::PHINX_TYPE_DECIMAL) {
$column->setPrecision($columnInfo['numeric_precision']);
}

if ($enumValues !== null) {
$column->setValues($enumValues);
}

$columns[] = $column;
}

Expand Down Expand Up @@ -543,11 +583,23 @@ public function hasColumn(string $tableName, string $columnName): bool
*/
protected function getAddColumnInstructions(Table $table, Column $column): AlterInstructions
{
if ($column->getType() === static::PHINX_TYPE_ENUM) {
$values = $column->getValues();
if (!empty($values)) {
$typeName = $this->getEnumTypeName($table->getName(), $column->getName());
$this->execute(sprintf(
'CREATE TYPE %s AS ENUM (%s)',
$this->quoteColumnName($typeName),
implode(', ', array_map(fn($v) => $this->getConnection()->quote($v), $values)),
));
}
}

$instructions = new AlterInstructions();
$instructions->addAlter(sprintf(
'ADD %s %s %s',
$this->quoteColumnName($column->getName()),
$this->getColumnSqlDefinition($column),
$this->getColumnSqlDefinition($column, $table->getName()),
$column->isIdentity() && $column->getGenerated() !== null && $this->useIdentity ?
sprintf('GENERATED %s AS IDENTITY', $column->getGenerated()) : '',
));
Expand Down Expand Up @@ -614,7 +666,7 @@ protected function getChangeColumnInstructions(
$sql = sprintf(
'ALTER COLUMN %s TYPE %s',
$quotedColumnName,
$this->getColumnSqlDefinition($newColumn),
$this->getColumnSqlDefinition($newColumn, $tableName),
);
if (in_array($newColumn->getType(), ['smallinteger', 'integer', 'biginteger'], true)) {
$sql .= sprintf(
Expand Down Expand Up @@ -735,7 +787,18 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
$this->quoteColumnName($columnName),
);

return new AlterInstructions([$alter]);
$instructions = new AlterInstructions([$alter]);

// Drop the associated Phinx-managed enum type if it exists
$typeName = $this->getEnumTypeName($tableName, $columnName);
if ($this->hasEnumType($typeName)) {
$instructions->addPostStep(sprintf(
'DROP TYPE %s',
$this->quoteColumnName($typeName),
));
}

return $instructions;
}

/**
Expand Down Expand Up @@ -1098,6 +1161,8 @@ public function getSqlType(Literal|string $type, ?int $limit = null): array
return ['name' => 'bytea'];
case static::PHINX_TYPE_INTERVAL:
return ['name' => 'interval'];
case static::PHINX_TYPE_ENUM:
return ['name' => 'enum'];
// Geospatial database types
// Spatial storage in Postgres is done via the PostGIS extension,
// which enables the use of the "geography" type in combination
Expand Down Expand Up @@ -1227,9 +1292,10 @@ public function dropDatabase($name): void
* Gets the PostgreSQL Column Definition for a Column object.
*
* @param \Phinx\Db\Table\Column $column Column
* @param string|null $tableName Table name, used to derive the enum type name when column type is 'enum'
* @return string
*/
protected function getColumnSqlDefinition(Column $column): string
protected function getColumnSqlDefinition(Column $column, ?string $tableName = null): string
{
$buffer = [];

Expand All @@ -1241,6 +1307,11 @@ protected function getColumnSqlDefinition(Column $column): string
} else {
$buffer[] = 'SERIAL';
}
} elseif ($column->getType() === static::PHINX_TYPE_ENUM) {
$typeName = $tableName !== null
? $this->getEnumTypeName($tableName, $column->getName())
: $column->getName();
$buffer[] = $this->quoteColumnName($typeName);
} elseif ($column->getType() instanceof Literal) {
$buffer[] = (string)$column->getType();
} else {
Expand Down Expand Up @@ -1295,6 +1366,68 @@ protected function getColumnSqlDefinition(Column $column): string
return implode(' ', $buffer);
}

/**
* Returns the Phinx-managed PostgreSQL enum type name for a given table column.
*
* The convention is `{table}_{column}` using the unqualified table name so that
* two tables cannot accidentally share the same type with different values.
*
* @param string $tableName Table name (may be schema-qualified)
* @param string $columnName Column name
* @return string
*/
protected function getEnumTypeName(string $tableName, string $columnName): string
{
return $this->getSchemaName($tableName)['table'] . '_' . $columnName;
}

/**
* Returns the enum label values for a given PostgreSQL enum type name, or null if the
* type does not exist or is not an enum.
*
* @param string $typeName Type name (unqualified)
* @return string[]|null
*/
protected function getEnumTypeValues(string $typeName): ?array
{
$sql = sprintf(
"SELECT e.enumlabel
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
JOIN pg_namespace n ON t.typnamespace = n.oid
WHERE t.typname = %s AND t.typtype = 'e' AND n.nspname = %s
ORDER BY e.enumsortorder",
$this->getConnection()->quote($typeName),
$this->getConnection()->quote($this->schema),
);
$rows = $this->fetchAll($sql);

return !empty($rows) ? array_column($rows, 'enumlabel') : null;
}

/**
* Returns true if a PostgreSQL enum type with the given unqualified name exists in the
* current schema.
*
* @param string $typeName Type name (unqualified)
* @return bool
*/
protected function hasEnumType(string $typeName): bool
{
$sql = sprintf(
"SELECT EXISTS(
SELECT 1 FROM pg_type t
JOIN pg_namespace n ON t.typnamespace = n.oid
WHERE t.typname = %s AND t.typtype = 'e' AND n.nspname = %s
) AS type_exists",
$this->getConnection()->quote($typeName),
$this->getConnection()->quote($this->schema),
);
$result = $this->fetchRow($sql);

return (bool)$result['type_exists'];
}

/**
* Gets the PostgreSQL Column Comment Definition for a column object.
*
Expand Down
4 changes: 2 additions & 2 deletions src/Phinx/Db/Table/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ class Column
/** MySQL-only column type */
public const MEDIUMINTEGER = AdapterInterface::PHINX_TYPE_MEDIUM_INTEGER;
/** MySQL-only column type */
public const ENUM = AdapterInterface::PHINX_TYPE_ENUM;
/** MySQL-only column type */
public const SET = AdapterInterface::PHINX_TYPE_STRING;
/** MySQL-only column type */
public const BLOB = AdapterInterface::PHINX_TYPE_BLOB;
/** MySQL-only column type */
public const YEAR = AdapterInterface::PHINX_TYPE_YEAR;
/** MySQL/Postgres-only column type */
public const ENUM = AdapterInterface::PHINX_TYPE_ENUM;
/** MySQL/Postgres-only column type */
public const JSON = AdapterInterface::PHINX_TYPE_JSON;
/** Postgres-only column type */
public const JSONB = AdapterInterface::PHINX_TYPE_JSONB;
Expand Down
Loading