diff --git a/README.md b/README.md index 597fbc33..b23c6e87 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,97 @@ $this->update('{{%company}}', ['name' => 'No name']); $this->alterColumn('{{%company}}', 'name', $this->string(128)->notNull()); ``` +### Handling of `NOT NULL` constraints + +`NOT NULL` in DB migrations is determined by `nullable` and `required` properties of the OpenAPI schema. +e.g. attribute = 'my_property'. + +- If you define attribute neither "required" nor via "nullable", then it is by default `NULL`: + +```yaml + ExampleSchema: + properties: + my_property: + type: string +``` + +- If you define attribute in "required", then it is `NOT NULL` + +```yaml + ExampleSchema: + required: + - my_property + properties: + my_property: + type: string +``` + +- If you define attribute via "nullable", then it overrides "required", e.g. allow `NULL` in this case: + +```yaml + ExampleSchema: + required: + - my_property + properties: + my_property: + type: string + nullable: true +``` + +- If you define attribute via "nullable", then it overrides "required", e.g. `NOT NULL` in this case: + +```yaml + test_table: + required: + properties: + my_property: + type: string + nullable: false +``` + +### Handling of `enum` (#enum, #MariaDb) +It work on MariaDb. + + ```yaml + test_table: + properties: + my_property: + enum: + - one + - two + - three +``` + +### Handling of `numeric` (#numeric, #MariaDb) +precision-default = 10 +scale-default = 2 + +- You can define attribute like "numeric(precision,scale)": + ```yaml + test_table: + properties: + my_property: + x-db-type: decimal(12,4) +``` +DB-Result = decimal(12,4) + +- You can define attribute like "numeric(precision)" with default scale-default = 2: + ```yaml + test_table: + properties: + my_property: + x-db-type: decimal(12) +``` +DB-Result = decimal(12,2) + +- You can define attribute like "numeric" with precision-default = 10 and scale-default = 2: + ```yaml + test_table: + properties: + my_property: + x-db-type: decimal +``` +DB-Result = decimal(10,2) ## Screenshots diff --git a/src/lib/AttributeResolver.php b/src/lib/AttributeResolver.php index 1b2e6a9a..d74287c7 100644 --- a/src/lib/AttributeResolver.php +++ b/src/lib/AttributeResolver.php @@ -203,6 +203,8 @@ protected function resolveProperty(PropertySchema $property, bool $isRequired):v ->setDescription($property->getAttr('description', '')) ->setReadOnly($property->isReadonly()) ->setDefault($property->guessDefault()) + ->setXDbType($property->getAttr('x-db-type', null)) + ->setNullable($property->getProperty()->getSerializableData()->nullable ?? null) ->setIsPrimary($property->isPrimaryKey()); if ($property->isReference()) { if ($property->isVirtual()) { diff --git a/src/lib/ColumnToCode.php b/src/lib/ColumnToCode.php index 9f3b3973..239a3709 100644 --- a/src/lib/ColumnToCode.php +++ b/src/lib/ColumnToCode.php @@ -147,7 +147,57 @@ public function isJson():bool public function isEnum():bool { - return StringHelper::startsWith($this->column->dbType, 'enum'); + return !empty($this->column->enumValues); + } + + public function isDecimal() + { + return self::isDecimalByDbType($this->column->dbType); + } + + /** + * @param $dbType + * @return array|false + */ + public static function isDecimalByDbType($dbType) + { + $precision = null; + $scale = null; + + // https://runebook.dev/de/docs/mariadb/decimal/index + $precisionDefault = 10; + $scaleDefault = 2; + + preg_match_all('/(decimal\()+(\d)+(,)+(\d)+(\))/', $dbType, $matches); + if (!empty($matches[4][0])) { + $precision = $matches[2][0]; + $scale = $matches[4][0]; + } + + if (empty($precision)){ + preg_match_all('/(decimal\()+(\d)+(\))/', $dbType, $matches); + if (!empty($matches[2][0])) { + $precision = $matches[2][0]; + $scale = $scaleDefault; + } + } + + if (empty($precision)){ + if (strtolower($dbType) === 'decimal'){ + $precision = $precisionDefault; + $scale = $scaleDefault; + } + } + + if (empty($precision)){ + return false; + } + + return [ + 'precision' => (int)$precision, + 'scale' => (int)$scale, + 'dbType' => "decimal($precision,$scale)", + ]; } public static function escapeQuotes(string $str):string @@ -208,11 +258,6 @@ private function resolve():void if ($dbType === 'varchar') { $type = $dbType = 'string'; } - if ($this->fromDb === true) { - $this->isBuiltinType = isset((new ColumnSchemaBuilder(''))->categoryMap[$type]); - } else { - $this->isBuiltinType = isset((new ColumnSchemaBuilder(''))->categoryMap[$dbType]); - } $fluentSize = $this->column->size ? '(' . $this->column->size . ')' : '()'; $rawSize = $this->column->size ? '(' . $this->column->size . ')' : ''; $this->rawParts['nullable'] = $this->column->allowNull ? 'NULL' : 'NOT NULL'; @@ -227,14 +272,37 @@ private function resolve():void $this->column->dbType . (strpos($this->column->dbType, '(') !== false ? '' : $rawSize); } elseif ($this->isEnum()) { $this->resolveEnumType(); + } elseif ($this->isDecimal()) { + $this->fluentParts['type'] = $dbType; + $this->rawParts['type'] = $dbType; } else { $this->fluentParts['type'] = $type . $fluentSize; $this->rawParts['type'] = $this->column->dbType . (strpos($this->column->dbType, '(') !== false ? '' : $rawSize); } + + $this->isBuiltinType = $this->getIsBuiltinType($type, $dbType); + $this->resolveDefaultValue(); } + /** + * @param $type + * @param $dbType + * @return bool + */ + private function getIsBuiltinType($type, $dbType) + { + if ($this->isEnum() && $this->isMariaDb()){ + return false; + } + if ($this->fromDb === true){ + return isset((new ColumnSchemaBuilder(''))->categoryMap[$type]); + } else { + return isset((new ColumnSchemaBuilder(''))->categoryMap[$dbType]); + } + } + private function resolveEnumType():void { if ($this->isPostgres()) { @@ -314,10 +382,18 @@ private function resolveDefaultValue():void private function isDefaultAllowed():bool { $type = strtolower($this->column->dbType); - if ($type === 'tsvector') { - return false; + switch ($type){ + case 'tsvector': + return false; + case 'blob': + case 'geometry': + case 'text': + case 'json': + return ($this->isMysql() && !$this->isMariaDb()) === false; + case 'enum': + default: + return true; } - return !($this->isMysql() && !$this->isMariaDb() && in_array($type, ['blob', 'geometry', 'text', 'json'])); } private function typeWithoutSize(string $type):string diff --git a/src/lib/items/Attribute.php b/src/lib/items/Attribute.php index a71f02e6..325dbc8d 100644 --- a/src/lib/items/Attribute.php +++ b/src/lib/items/Attribute.php @@ -46,6 +46,18 @@ class Attribute extends BaseObject */ public $dbType = 'string'; + /** + * Custom db type + * string | null + */ + public $xDbType; + + /** + * nullable + * bool | null + */ + public $nullable; + /** * @var string */ @@ -117,6 +129,18 @@ public function setDbType(string $dbType):Attribute return $this; } + public function setXDbType($xDbType):Attribute + { + $this->xDbType = $xDbType; + return $this; + } + + public function setNullable($nullable):Attribute + { + $this->nullable = $nullable; + return $this; + } + public function setDescription(string $description):Attribute { $this->description = $description; @@ -247,7 +271,7 @@ public function toColumnSchema():ColumnSchema 'phpType'=>$this->phpType, 'dbType' => strtolower($this->dbType), 'type' => $this->dbTypeAbstract($this->dbType), - 'allowNull' => !$this->isRequired(), + 'allowNull' => $this->allowNull(), 'size' => $this->size > 0 ? $this->size : null, ]); $column->isPrimaryKey = $this->primary; @@ -270,6 +294,13 @@ public function toColumnSchema():ColumnSchema private function dbTypeAbstract(string $type):string { + /** + * Custom db type + */ + if ($this->xDbType){ + return $this->xDbType; + } + if (stripos($type, 'int') === 0) { return 'integer'; } @@ -290,4 +321,12 @@ private function dbTypeAbstract(string $type):string } return $type; } + + private function allowNull() + { + if (is_bool($this->nullable)){ + return $this->nullable; + } + return !$this->isRequired(); + } } diff --git a/src/lib/items/DbModel.php b/src/lib/items/DbModel.php index c306b877..dbfb09cc 100644 --- a/src/lib/items/DbModel.php +++ b/src/lib/items/DbModel.php @@ -139,8 +139,7 @@ public function getEnumAttributes():array return array_filter( $this->attributes, static function (Attribute $attribute) { - return !$attribute->isVirtual && StringHelper::startsWith($attribute->dbType, 'enum') - && !empty($attribute->enumValues); + return !$attribute->isVirtual && !empty($attribute->enumValues); } ); } diff --git a/src/lib/migrations/BaseMigrationBuilder.php b/src/lib/migrations/BaseMigrationBuilder.php index 781e7732..740ef9a8 100644 --- a/src/lib/migrations/BaseMigrationBuilder.php +++ b/src/lib/migrations/BaseMigrationBuilder.php @@ -205,6 +205,10 @@ function (string $unknownColumn) { $current->type = 'enum'; $current->dbType = 'enum'; } + if (!empty($desired->enumValues)) { + $desired->type = 'enum'; + $desired->dbType = 'enum'; + } $changedAttributes = $this->compareColumns($current, $desired); if (empty($changedAttributes)) { continue; diff --git a/src/lib/migrations/MysqlMigrationBuilder.php b/src/lib/migrations/MysqlMigrationBuilder.php index 65029fcd..eadb8fbd 100644 --- a/src/lib/migrations/MysqlMigrationBuilder.php +++ b/src/lib/migrations/MysqlMigrationBuilder.php @@ -7,6 +7,7 @@ namespace cebe\yii2openapi\lib\migrations; +use cebe\yii2openapi\lib\ColumnToCode; use cebe\yii2openapi\lib\items\DbIndex; use yii\base\NotSupportedException; use yii\db\ColumnSchema; @@ -56,6 +57,19 @@ protected function compareColumns(ColumnSchema $current, ColumnSchema $desired): if ($current->type === $desired->type && !$desired->size && $this->isDbDefaultSize($current)) { $desired->size = $current->size; } + + if ($decimalAttributes = ColumnToCode::isDecimalByDbType($desired->dbType)){ + $desired->precision = $decimalAttributes['precision']; + $desired->scale = $decimalAttributes['scale']; + $desired->type = 'decimal'; + $desired->size = $decimalAttributes['precision']; + foreach (['precision', 'scale', 'dbType'] as $decimalAttr) { + if ($current->$decimalAttr !== $desired->$decimalAttr) { + $changedAttributes[] = $decimalAttr; + } + } + } + foreach (['type', 'size', 'allowNull', 'defaultValue', 'enumValues'] as $attr) { if ($current->$attr !== $desired->$attr) { $changedAttributes[] = $attr; @@ -66,7 +80,7 @@ protected function compareColumns(ColumnSchema $current, ColumnSchema $desired): protected function createEnumMigrations():void { - // Postgres only case + // execute via default } protected function isDbDefaultSize(ColumnSchema $current):bool