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
162 changes: 155 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,26 @@ 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)) {
throw new InvalidArgumentException(sprintf(
'Column "%s" is of type enum but has no values defined.',
$column->getName(),
));
}
$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)),
);
}
Comment thread
ErfanMomeniii marked this conversation as resolved.
}

$queries[] = $sql;

// process column comments
Expand Down Expand Up @@ -421,10 +442,26 @@ protected function getRenameTableInstructions(string $tableName, string $newTabl
*/
protected function getDropTableInstructions(string $tableName): AlterInstructions
{
$parts = $this->getSchemaName($tableName);
$this->removeCreatedTable($tableName);
$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
$instructions = new AlterInstructions([], [$sql]);

return new AlterInstructions([], [$sql]);
// Drop any Phinx-managed enum types associated with this table's enum columns
foreach ($this->getColumns($tableName) as $column) {
if ($column->getType() !== static::PHINX_TYPE_ENUM) {
continue;
}
$typeName = $this->getEnumTypeName($tableName, $column->getName());
if ($this->hasEnumType($typeName, $parts['schema'])) {
$instructions->addPostStep(sprintf(
'DROP TYPE %s',
$this->quoteColumnName($typeName),
));
}
}

return $instructions;
}

/**
Expand Down Expand Up @@ -462,9 +499,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'], $parts['schema']);
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 +555,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 @@ -544,10 +592,26 @@ public function hasColumn(string $tableName, string $columnName): bool
protected function getAddColumnInstructions(Table $table, Column $column): AlterInstructions
{
$instructions = new AlterInstructions();

if ($column->getType() === static::PHINX_TYPE_ENUM) {
$values = $column->getValues();
if (empty($values)) {
throw new InvalidArgumentException(sprintf(
'Column "%s" is of type enum but has no values defined.',
$column->getName(),
));
}
$typeName = $this->getEnumTypeName($table->getName(), $column->getName());
$instructions->addPreStep(sprintf(
'CREATE TYPE %s AS ENUM (%s)',
$this->quoteColumnName($typeName),
implode(', ', array_map(fn($v) => $this->getConnection()->quote($v), $values)),
));
}
Comment thread
ErfanMomeniii marked this conversation as resolved.
$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 +678,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 +799,19 @@ 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
$parts = $this->getSchemaName($tableName);
$typeName = $this->getEnumTypeName($tableName, $columnName);
if ($this->hasEnumType($typeName, $parts['schema'])) {
$instructions->addPostStep(sprintf(
'DROP TYPE %s',
$this->quoteColumnName($typeName),
));
}

return $instructions;
}

/**
Expand Down Expand Up @@ -1098,6 +1174,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 +1305,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 +1320,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 +1379,70 @@ 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)
* @param string|null $schemaName Schema name (defaults to adapter schema)
* @return string[]|null
*/
protected function getEnumTypeValues(string $typeName, ?string $schemaName = null): ?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($schemaName ?? $this->schema),
);
Comment thread
ErfanMomeniii marked this conversation as resolved.
$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
* given schema.
*
* @param string $typeName Type name (unqualified)
* @param string|null $schemaName Schema name (defaults to adapter schema)
* @return bool
*/
protected function hasEnumType(string $typeName, ?string $schemaName = null): 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($schemaName ?? $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
36 changes: 36 additions & 0 deletions src/Phinx/Db/Util/AlterInstructions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
*/
class AlterInstructions
{
/**
* @var (string|callable)[] The SQL commands to be executed before the ALTER instruction
*/
protected array $preSteps = [];

/**
* @var string[] The SQL snippets to be added to an ALTER instruction
*/
Expand Down Expand Up @@ -48,6 +53,27 @@ public function __construct(array $alterParts = [], array $postSteps = [])
$this->postSteps = $postSteps;
}

/**
* Adds a SQL command to be executed before the ALTER instruction.
*
* @param string|callable $sql The SQL to run before, or a callable to execute
* @return void
*/
public function addPreStep(string|callable $sql): void
{
$this->preSteps[] = $sql;
}

/**
* Returns the SQL commands to run before the ALTER instruction
*
* @return (string|callable)[]
*/
public function getPreSteps(): array
{
return $this->preSteps;
}

/**
* Adds another part to the single ALTER instruction
*
Expand Down Expand Up @@ -146,6 +172,7 @@ public function getLock(): ?string
*/
public function merge(AlterInstructions $other): void
{
$this->preSteps = array_merge($this->preSteps, $other->getPreSteps());
$this->alterParts = array_merge($this->alterParts, $other->getAlterParts());
$this->postSteps = array_merge($this->postSteps, $other->getPostSteps());

Expand Down Expand Up @@ -182,6 +209,15 @@ public function merge(AlterInstructions $other): void
*/
public function execute(string $alterTemplate, callable $executor): void
{
foreach ($this->preSteps as $instruction) {
if (is_callable($instruction)) {
$instruction();
continue;
}

$executor($instruction);
}

if ($this->alterParts) {
$alter = sprintf($alterTemplate, implode(', ', $this->alterParts));
$executor($alter);
Expand Down
Loading