diff --git a/src/db/ColumnSchema.php b/src/db/ColumnSchema.php index 71127a24..64ff042f 100644 --- a/src/db/ColumnSchema.php +++ b/src/db/ColumnSchema.php @@ -25,4 +25,11 @@ class ColumnSchema extends \yii\db\ColumnSchema * ``` */ public $xDbType; + + /** + * @var string|null + * Custom Enum type + * @see \cebe\yii2openapi\lib\items\Attribute::$xDbType and `x-db-type` docs in README.md // TODO SK + */ + public $xEnumType; } diff --git a/src/lib/AttributeResolver.php b/src/lib/AttributeResolver.php index 906226d4..77ffb59c 100644 --- a/src/lib/AttributeResolver.php +++ b/src/lib/AttributeResolver.php @@ -235,7 +235,7 @@ protected function resolveProperty( if ($property->isVirtual()) { throw new InvalidDefinitionException('References not supported for virtual attributes'); } - + if ($property->isNonDbReference()) { $attribute->asNonDbReference($property->getRefClassName()); $relation = Yii::createObject( @@ -288,6 +288,9 @@ protected function resolveProperty( if ($property->hasEnum()) { $attribute->setEnumValues($property->getAttr('enum')); } + if ($property->hasAttr('x-enum-type')) { + $attribute->setXEnumType($property->getAttr('x-enum-type')); + } } if ($property->hasRefItems()) { diff --git a/src/lib/ColumnToCode.php b/src/lib/ColumnToCode.php index e9a570bc..7862b37d 100644 --- a/src/lib/ColumnToCode.php +++ b/src/lib/ColumnToCode.php @@ -188,7 +188,7 @@ public function getAlterExpression(bool $addUsingExpression = false):string { if ($this->isEnum() && ApiGenerator::isPostgres()) { $rawTableName = $this->dbSchema->getRawTableName($this->tableAlias); - $enumTypeName = 'enum_'.$rawTableName.'_'.$this->column->name; + $enumTypeName = $this->column->xEnumType ?? 'enum_'.$rawTableName.'_'.$this->column->name; return "'" . sprintf('"'.$enumTypeName.'" USING "%1$s"::"'.$enumTypeName.'"', $this->column->name) . "'"; } if ($this->column->dbType === 'tsvector') { @@ -399,7 +399,7 @@ private function resolveEnumType():void { if (ApiGenerator::isPostgres()) { $rawTableName = $this->dbSchema->getRawTableName($this->tableAlias); - $this->rawParts['type'] = '"enum_'.$rawTableName.'_' . $this->column->name.'"'; + $this->rawParts['type'] = $this->column->xEnumType ?? '"enum_'.$rawTableName.'_' . $this->column->name.'"'; return; } $this->rawParts['type'] = 'enum(' . self::mysqlEnumToString($this->column->enumValues) . ')'; diff --git a/src/lib/items/Attribute.php b/src/lib/items/Attribute.php index 5b62e49d..77e63158 100644 --- a/src/lib/items/Attribute.php +++ b/src/lib/items/Attribute.php @@ -115,6 +115,12 @@ class Attribute extends BaseObject */ public $defaultValue; + /** + * Custom enum type naming + * string | null + */ + public $xEnumType; + /** * @var array|null */ @@ -200,6 +206,12 @@ public function setDefault($value):Attribute return $this; } + public function setXEnumType(string $xEnumType):Attribute + { + $this->xEnumType = $xEnumType; + return $this; + } + public function setEnumValues(array $values):Attribute { $this->enumValues = $values; @@ -330,6 +342,9 @@ public function toColumnSchema():ColumnSchema //@TODO: Need to discuss $column->defaultValue = null; } + if (!empty($this->xEnumType)) { + $column->xEnumType = $this->xEnumType; + } if (is_array($this->enumValues)) { $column->enumValues = $this->enumValues; } diff --git a/src/lib/migrations/BaseMigrationBuilder.php b/src/lib/migrations/BaseMigrationBuilder.php index d4c49c21..e5038d7b 100644 --- a/src/lib/migrations/BaseMigrationBuilder.php +++ b/src/lib/migrations/BaseMigrationBuilder.php @@ -420,8 +420,13 @@ protected function isNeedUsingExpression(string $fromDbType, string $toDbType):b public function tmpSaveNewCol(string $tableAlias, \cebe\yii2openapi\db\ColumnSchema $columnSchema): \yii\db\ColumnSchema { $tmpTableName = 'tmp_table_'; - $tmpEnumName = function (string $columnName): string { - return '"tmp_enum_'.$columnName.'_"'; + + $tmpEnumName = function (\cebe\yii2openapi\db\ColumnSchema $columnSchema): string { + if ($columnSchema->xEnumType) { +// return 'tmp_'.$columnSchema->xEnumType; + return $columnSchema->xEnumType; + } + return '"tmp_enum_'.$columnSchema->name.'_"'; }; $rawTableName = $this->db->schema->getRawTableName($tableAlias); $innerEnumTypeName = "\"enum_{$tmpTableName}_{$columnSchema->name}\""; @@ -432,12 +437,12 @@ public function tmpSaveNewCol(string $tableAlias, \cebe\yii2openapi\db\ColumnSch $name = MigrationRecordBuilder::quote($columnSchema->name); $column = [$name.' '.$this->newColStr($tmpTableName, $columnSchema)]; if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) { - $column = strtr($column, [$innerEnumTypeName => $tmpEnumName($columnSchema->name)]); + $column = strtr($column, [$innerEnumTypeName => $tmpEnumName($columnSchema)]); } } else { $column = [$columnSchema->name => $this->newColStr($tmpTableName, $columnSchema)]; if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) { - $column[$columnSchema->name] = strtr($column[$columnSchema->name], [$innerEnumTypeName => $tmpEnumName($columnSchema->name)]); + $column[$columnSchema->name] = strtr($column[$columnSchema->name], [$innerEnumTypeName => $tmpEnumName($columnSchema)]); } } @@ -448,7 +453,7 @@ public function tmpSaveNewCol(string $tableAlias, \cebe\yii2openapi\db\ColumnSch return "'$aValue'"; }, $allEnumValues); Yii::$app->db->createCommand( - 'CREATE TYPE '.$tmpEnumName($columnSchema->name).' AS ENUM('.implode(', ', $allEnumValues).')' + 'CREATE TYPE '.$tmpEnumName($columnSchema).' AS ENUM('.implode(', ', $allEnumValues).')' )->execute(); } @@ -459,13 +464,25 @@ public function tmpSaveNewCol(string $tableAlias, \cebe\yii2openapi\db\ColumnSch Yii::$app->db->createCommand()->dropTable($tmpTableName)->execute(); if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {// drop enum - Yii::$app->db->createCommand('DROP TYPE '.$tmpEnumName($columnSchema->name))->execute(); - if ('"'.$table->columns[$columnSchema->name]->dbType.'"' !== $tmpEnumName($columnSchema->name)) { - throw new \Exception('Unknown error related to PgSQL enum '.$table->columns[$columnSchema->name]->dbType); - } + Yii::$app->db->createCommand('DROP TYPE '.$tmpEnumName($columnSchema))->execute(); + +// $table->columns[$columnSchema->name]->dbType = $tmpEnumName($columnSchema); + $table->columns[$columnSchema->name]->dbType = $columnSchema->xEnumType ?? "enum_{$rawTableName}_{$columnSchema->name}"; + + +// if ('"'.$table->columns[$columnSchema->name]->dbType.'"' !== $tmpEnumName($columnSchema)) { +// throw new \Exception('Unknown error related to PgSQL enum '.$table->columns[$columnSchema->name]->dbType); +// } // reset back column enum name to original as we are comparing with current // e.g. we get different enum type name such as `enum_status` and `tmp_enum_status_` even there is no change, so below statement fix this issue - $table->columns[$columnSchema->name]->dbType = 'enum_'.$rawTableName.'_'.$columnSchema->name; +// $table->columns[$columnSchema->name]->dbType = $columnSchema->xEnumType ?? 'enum_'.$rawTableName.'_'.$columnSchema->name; +// $table->columns[$columnSchema->name]->dbType = 'enum_'.$rawTableName.'_'.$columnSchema->name; + + +// if (is_array($desired->enumValues)) { +// $desired->dbType = $columnSchema->xEnumType ?? 'enum_'.$rawTableName.'_'.$desired->name +// } + } return $table->columns[$columnSchema->name]; diff --git a/src/lib/migrations/MigrationRecordBuilder.php b/src/lib/migrations/MigrationRecordBuilder.php index 98ee03b8..af8799db 100644 --- a/src/lib/migrations/MigrationRecordBuilder.php +++ b/src/lib/migrations/MigrationRecordBuilder.php @@ -27,8 +27,8 @@ final class MigrationRecordBuilder public const ADD_UNIQUE = MigrationRecordBuilder::INDENT . "\$this->createIndex('%s', '%s', %s, true);"; public const ADD_INDEX = MigrationRecordBuilder::INDENT . "\$this->createIndex('%s', '%s', %s, %s);"; public const DROP_COLUMN = MigrationRecordBuilder::INDENT . "\$this->dropColumn('%s', '%s');"; - public const ADD_ENUM = MigrationRecordBuilder::INDENT . "\$this->execute('CREATE TYPE \"enum_%s_%s\" AS ENUM(%s)');"; - public const DROP_ENUM = MigrationRecordBuilder::INDENT . "\$this->execute('DROP TYPE \"enum_%s_%s\"');"; + public const ADD_ENUM = MigrationRecordBuilder::INDENT . "\$this->execute('CREATE TYPE \"%s\" AS ENUM(%s)');"; + public const DROP_ENUM = MigrationRecordBuilder::INDENT . "\$this->execute('DROP TYPE \"%s\"');"; public const DROP_TABLE = MigrationRecordBuilder::INDENT . "\$this->dropTable('%s');"; public const ADD_FK = MigrationRecordBuilder::INDENT . "\$this->addForeignKey('%s', '%s', '%s', '%s', '%s');"; @@ -203,10 +203,20 @@ public function dropColumnNotNull(string $tableAlias, ColumnSchema $column):stri return sprintf(self::ALTER_COLUMN, $tableAlias, $column->name, '"DROP NOT NULL"'); } - public function createEnum(string $tableAlias, string $columnName, array $values):string + public function createEnum(string $tableAlias, string $columnName, array $values, ?string $enumType = null): string { $rawTableName = $this->dbSchema->getRawTableName($tableAlias); - return sprintf(self::ADD_ENUM, $rawTableName, $columnName, ColumnToCode::enumToString($values)); + + if (is_string($enumType)) { + $enumType = trim($enumType); + } + + // -- Decide the final enum type name + // If a custom name is provided, use it verbatim (trim extra quotes). + // Otherwise derive the legacy name "enum__" (old behavior). + $typeName = $enumType ?: sprintf('enum_%s_%s', $rawTableName, $columnName); + + return sprintf(self::ADD_ENUM, $typeName, ColumnToCode::enumToString($values)); } public function addFk(string $fkName, string $tableAlias, string $fkCol, string $refTable, string $refCol, ?string $onDelete = null, ?string $onUpdate = null):string diff --git a/src/lib/migrations/PostgresMigrationBuilder.php b/src/lib/migrations/PostgresMigrationBuilder.php index b8c9324d..bf81038c 100644 --- a/src/lib/migrations/PostgresMigrationBuilder.php +++ b/src/lib/migrations/PostgresMigrationBuilder.php @@ -23,7 +23,7 @@ protected function buildColumnsCreation(array $columns):void foreach ($columns as $column) { $tableName = $this->model->getTableAlias(); if (static::isEnum($column)) { - $this->migration->addUpCode($this->recordBuilder->createEnum($tableName, $column->name, $column->enumValues)) + $this->migration->addUpCode($this->recordBuilder->createEnum($tableName, $column->name, $column->enumValues, $column->xEnumType)) ->addDownCode($this->recordBuilder->dropEnum($tableName, $column->name), true); } $this->migration->addUpCode($this->recordBuilder->addColumn($tableName, $column)) @@ -42,7 +42,7 @@ protected function buildColumnsDrop(array $columns):void $this->migration->addDownCode($this->recordBuilder->addDbColumn($tableName, $column)) ->addUpCode($this->recordBuilder->dropColumn($tableName, $column->name)); if (static::isEnum($column)) { - $this->migration->addDownCode($this->recordBuilder->createEnum($tableName, $column->name, $column->enumValues)) + $this->migration->addDownCode($this->recordBuilder->createEnum($tableName, $column->name, $column->enumValues, $column->xEnumType)) ->addUpCode($this->recordBuilder->dropEnum($tableName, $column->name)); } } @@ -92,7 +92,7 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir } } if ($isChangeToEnum) { - $this->migration->addUpCode($this->recordBuilder->createEnum($tableName, $desired->name, $desired->enumValues), true); + $this->migration->addUpCode($this->recordBuilder->createEnum($tableName, $desired->name, $desired->enumValues, $desired->xEnumType), true); } if ($isChangeFromEnum) { $this->migration->addUpCode($this->recordBuilder->dropEnum($tableName, $current->name)); @@ -100,7 +100,7 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir if ($isChangeFromEnum) { $this->migration - ->addDownCode($this->recordBuilder->createEnum($tableName, $current->name, $current->enumValues)); + ->addDownCode($this->recordBuilder->createEnum($tableName, $current->name, $current->enumValues, $current->xEnumType)); } if ($isChangeToEnum) { $this->migration->addDownCode($this->recordBuilder->dropEnum($tableName, $current->name), true); @@ -125,6 +125,7 @@ protected function compareColumns(ColumnSchema $current, ColumnSchema $desired): $this->modifyDesiredInContextOfCurrent($current, $desiredFromDb); $this->modifyDesiredFromDbInContextOfDesired($desired, $desiredFromDb); + // TODO SK foreach (['type', 'size', 'allowNull', 'defaultValue', 'enumValues' , 'dbType', 'phpType' , 'precision', 'scale', 'unsigned' @@ -152,7 +153,7 @@ protected function createEnumMigrations():void continue; } $this->migration - ->addUpCode($this->recordBuilder->createEnum($tableAlias, $attr->columnName, $attr->enumValues), true) + ->addUpCode($this->recordBuilder->createEnum($tableAlias, $attr->columnName, $attr->enumValues, $attr->xEnumType), true) ->addDownCode($this->recordBuilder->dropEnum($tableAlias, $attr->columnName), true); } } @@ -248,4 +249,36 @@ public function modifyDesiredInContextOfCurrent(ColumnSchema $current, ColumnSch $desired->size = $current->size; } } + + +// /** +// * Get PostgreSQL enum type name for a specific column. +// * +// * @param string $schema e.g. 'public' +// * @param string $table table name without schema quotes, e.g. 'suggestions' +// * @param string $column column name, e.g. 'type' +// * @return string|null enum type name like 'enum_suggestions_united_type' or null if not enum/user-defined +// */ +// function pgEnumTypeName(string $schema, string $table, string $column): ?string +// { +// // information_schema exposes enum/domains as USER-DEFINED with udt_name = type name +// $sql = <<db->createCommand($sql, [ +// ':schema' => $schema, +// ':table' => $table, +// ':column' => $column, +// ])->queryScalar(); +// +// return $type !== false ? $type : null; +// } + } diff --git a/src/lib/openapi/PropertySchema.php b/src/lib/openapi/PropertySchema.php index 6b27def1..62246686 100644 --- a/src/lib/openapi/PropertySchema.php +++ b/src/lib/openapi/PropertySchema.php @@ -390,6 +390,22 @@ public function isReadonly():bool public function guessPhpType():string { + if (isset($this->property->{'x-enum-type'})) { + return 'string'; + } +// +// +// // Keep original DB type string (do NOT lowercase yet) +// $rawDbType = isset($this->property->{CustomSpecAttr::DB_TYPE}) +// ? (string)$this->property->{CustomSpecAttr::DB_TYPE} +// : null; +// +// // --- Special-case: x-db-type: enum() +// if ($rawDbType && preg_match('/^enum\(\s*("?[^")]+"?)\s*\)$/i', trim($rawDbType))) { +// // Treat as string in PHP regardless of DB enum +// return 'string'; +// } + $customDbType = isset($this->property->{CustomSpecAttr::DB_TYPE}) ? strtolower($this->property->{CustomSpecAttr::DB_TYPE}) : null; if ($customDbType !== null