Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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