Skip to content

Commit a5584d1

Browse files
authored
Merge pull request cebe#124 from SOHELAHMED7/master
Merge PRs of SOHELAHMED7/yii2-openapi
2 parents 1ed18c6 + 8250c09 commit a5584d1

File tree

148 files changed

+3604
-556
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+3604
-556
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,8 @@ e.g. attribute = 'my_property'.
331331
nullable: false
332332
```
333333

334-
### Handling of `enum` (#enum, #MariaDb)
335-
It work on MariaDb.
334+
### Handling of `enum` (#enum)
335+
It works on all 3 DB: MySQL, MariaDb and PgSQL.
336336

337337
```yaml
338338
test_table:
@@ -344,6 +344,8 @@ It work on MariaDb.
344344
- three
345345
```
346346

347+
Note: Change in enum values are not very simple. For Mysql and Mariadb, migrations will be generated but in many cases custom modification in it are required. For Pgsql migrations for change in enum values will not be generated. It should be handled manually.
348+
347349
### Handling of `numeric` (#numeric, #MariaDb)
348350

349351
precision-default = 10
@@ -390,9 +392,10 @@ Generated files:
390392

391393
# Development
392394

393-
There commands are available to develop and check the tests. It can be used inside the Docker container. To enter into bash of container run `make cli` .
395+
There commands are available to develop and check the tests. It is available inside the Docker container. To enter into bash shell of container, run `make cli` .
394396

395397
```bash
398+
cd tests
396399
./yii migrate-mysql/up
397400
./yii migrate-mysql/down 4
398401
@@ -424,4 +427,3 @@ Professional support, consulting as well as software development services are av
424427
https://www.cebe.cc/en/contact
425428

426429
Development of this library is sponsored by [cebe.:cloud: "Your Professional Deployment Platform"](https://cebe.cloud).
427-

src/lib/ColumnToCode.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ class ColumnToCode
5353
*/
5454
private $dbSchema;
5555

56+
/**
57+
* @var string
58+
* @example {{%table}}
59+
*/
60+
private $tableAlias;
61+
5662
/**
5763
* @var bool
5864
*/
@@ -107,13 +113,15 @@ class ColumnToCode
107113
*/
108114
public function __construct(
109115
Schema $dbSchema,
116+
string $tableAlias,
110117
ColumnSchema $column,
111118
bool $fromDb = false,
112119
bool $alter = false,
113120
bool $raw = false,
114121
bool $alterByXDbType = false
115122
) {
116123
$this->dbSchema = $dbSchema;
124+
$this->tableAlias = $tableAlias;
117125
$this->column = $column;
118126
$this->fromDb = $fromDb;
119127
$this->alter = $alter;
@@ -148,8 +156,8 @@ public function getCode(bool $quoted = false):string
148156
}
149157

150158
$code = $this->rawParts['type'] . ' ' . $this->rawParts['nullable'] . $default;
151-
if (ApiGenerator::isMysql() && $this->isEnum()) {
152-
return $quoted ? '"' . str_replace("\'", "'", $code) . '"' : $code;
159+
if ((ApiGenerator::isMysql() || ApiGenerator::isMariaDb()) && $this->isEnum()) {
160+
return $quoted ? "'" . $code . "'" : $code;
153161
}
154162
if (ApiGenerator::isPostgres() && $this->alterByXDbType) {
155163
return $quoted ? "'" . $this->rawParts['type'] . "'" : $this->rawParts['type'];
@@ -160,13 +168,18 @@ public function getCode(bool $quoted = false):string
160168
public function getAlterExpression(bool $addUsingExpression = false):string
161169
{
162170
if ($this->isEnum() && ApiGenerator::isPostgres()) {
163-
return "'" . sprintf('enum_%1$s USING %1$s::enum_%1$s', $this->column->name) . "'";
171+
$rawTableName = $this->dbSchema->getRawTableName($this->tableAlias);
172+
$enumTypeName = 'enum_'.$rawTableName.'_'.$this->column->name;
173+
return "'" . sprintf('"'.$enumTypeName.'" USING "%1$s"::"'.$enumTypeName.'"', $this->column->name) . "'";
164174
}
165175
if ($this->column->dbType === 'tsvector') {
166176
return "'" . $this->rawParts['type'] . "'";
167177
}
168178
if ($addUsingExpression && ApiGenerator::isPostgres()) {
169-
return "'" . $this->rawParts['type'] . " ".$this->rawParts['nullable']
179+
return "'" . $this->rawParts['type'] .
180+
($this->alterByXDbType ?
181+
'' :
182+
" ".$this->rawParts['nullable'])
170183
.' USING "'.$this->column->name.'"::'.$this->typeWithoutSize($this->rawParts['type'])."'";
171184
}
172185

@@ -270,7 +283,9 @@ public static function enumToString(array $enum):string
270283

271284
public static function mysqlEnumToString(array $enum):string
272285
{
273-
return implode(', ', array_map('self::wrapQuotes', $enum));
286+
return implode(', ', array_map(function ($aEnumValue) {
287+
return self::wrapQuotes($aEnumValue, '"');
288+
}, $enum));
274289
}
275290

276291
private function defaultValueJson(array $value):string
@@ -329,7 +344,7 @@ private function resolve():void
329344
$this->rawParts['type'] =
330345
$this->column->dbType . (strpos($this->column->dbType, '(') !== false ? '' : $rawSize);
331346
}
332-
347+
333348
$this->isBuiltinType = $this->raw ? false : $this->getIsBuiltinType($type, $dbType);
334349

335350
$this->resolveDefaultValue();
@@ -346,7 +361,7 @@ private function getIsBuiltinType($type, $dbType)
346361
return false;
347362
}
348363

349-
if ($this->isEnum() && ApiGenerator::isMariaDb()) {
364+
if ($this->isEnum()) {
350365
return false;
351366
}
352367
if ($this->fromDb === true) {
@@ -363,7 +378,8 @@ private function getIsBuiltinType($type, $dbType)
363378
private function resolveEnumType():void
364379
{
365380
if (ApiGenerator::isPostgres()) {
366-
$this->rawParts['type'] = 'enum_' . $this->column->name;
381+
$rawTableName = $this->dbSchema->getRawTableName($this->tableAlias);
382+
$this->rawParts['type'] = '"enum_'.$rawTableName.'_' . $this->column->name.'"';
367383
return;
368384
}
369385
$this->rawParts['type'] = 'enum(' . self::mysqlEnumToString($this->column->enumValues) . ')';
@@ -421,16 +437,18 @@ private function resolveDefaultValue():void
421437
break;
422438
default:
423439
$isExpression = StringHelper::startsWith($value, 'CURRENT')
440+
|| StringHelper::startsWith($value, 'current')
424441
|| StringHelper::startsWith($value, 'LOCAL')
425442
|| substr($value, -1, 1) === ')';
426443
if ($isExpression) {
427444
$this->fluentParts['default'] = 'defaultExpression("' . self::escapeQuotes((string)$value) . '")';
445+
$this->rawParts['default'] = $value;
428446
} else {
429447
$this->fluentParts['default'] = $expectInteger
430448
? 'defaultValue(' . $value . ')' : 'defaultValue("' . self::escapeQuotes((string)$value) . '")';
449+
$this->rawParts['default'] = $expectInteger ? $value : self::wrapQuotes($value);
431450
}
432-
$this->rawParts['default'] = $expectInteger ? $value : self::wrapQuotes($value);
433-
if (ApiGenerator::isMysql() && $this->isEnum()) {
451+
if ((ApiGenerator::isMysql() || ApiGenerator::isMariaDb()) && $this->isEnum()) {
434452
$this->rawParts['default'] = self::escapeQuotes($this->rawParts['default']);
435453
}
436454
}

src/lib/ValidationRulesBuilder.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public function build():array
6363
}
6464
}
6565
foreach ($this->model->attributes as $attribute) {
66+
// column/field/property with name `id` is considered as Primary Key by this library and it is automatically handled by DB/Yii; so remove it from validation `rules()`
67+
if ($attribute->columnName === 'id' || $attribute->propertyName === 'id') {
68+
continue;
69+
}
6670
$this->resolveAttributeRules($attribute);
6771
}
6872

@@ -181,6 +185,10 @@ private function prepareTypeScope():void
181185
if ($attribute->isReadOnly()) {
182186
continue;
183187
}
188+
// column/field/property with name `id` is considered as Primary Key by this library and it is automatically handled by DB/Yii; so remove it from validation `rules()`
189+
if ($attribute->columnName === 'id' || $attribute->propertyName === 'id') {
190+
continue;
191+
}
184192
if ($attribute->defaultValue === null && $attribute->isRequired()) {
185193
$this->typeScope['required'][$attribute->columnName] = $attribute->columnName;
186194
}

src/lib/migrations/BaseMigrationBuilder.php

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace cebe\yii2openapi\lib\migrations;
99

10+
use cebe\yii2openapi\generator\ApiGenerator;
1011
use cebe\yii2openapi\lib\ColumnToCode;
1112
use cebe\yii2openapi\lib\items\DbModel;
1213
use cebe\yii2openapi\lib\items\ManyToManyRelation;
@@ -15,6 +16,7 @@
1516
use yii\db\ColumnSchema;
1617
use yii\helpers\VarDumper;
1718
use yii\db\Connection;
19+
use yii\db\Expression;
1820

1921
abstract class BaseMigrationBuilder
2022
{
@@ -203,14 +205,6 @@ function (string $unknownColumn) {
203205
// do not adjust existing primary keys
204206
continue;
205207
}
206-
if (!empty($current->enumValues)) {
207-
$current->type = 'enum';
208-
$current->dbType = 'enum';
209-
}
210-
if (!empty($desired->enumValues)) {
211-
$desired->type = 'enum';
212-
$desired->dbType = 'enum';
213-
}
214208
$changedAttributes = $this->compareColumns($current, $desired);
215209
if (empty($changedAttributes)) {
216210
continue;
@@ -406,44 +400,111 @@ protected function unPrefixTableName(string $tableName):string
406400
return str_replace($this->db->tablePrefix, '', $tableName);
407401
}
408402

409-
protected function isNeedUsingExpression(string $fromType, string $toType):bool
403+
protected function isNeedUsingExpression(string $fromDbType, string $toDbType):bool
410404
{
411-
$strings = ['string', 'text', 'char'];
412-
if (in_array($fromType, $strings) && in_array($toType, $strings)) {
413-
return false;
414-
}
415-
$ints = ['smallint', 'integer', 'bigint', 'float', 'decimal'];
416-
if (in_array($fromType, $ints) && in_array($toType, $ints)) {
405+
if ($fromDbType === $toDbType) {
417406
return false;
418407
}
419-
$dates = ['date', 'timestamp'];
420-
return !(in_array($fromType, $dates) && in_array($toType, $dates));
408+
return true;
421409
}
422410

423-
public function tmpSaveNewCol(\cebe\yii2openapi\db\ColumnSchema $columnSchema): \yii\db\ColumnSchema
411+
// temporary save new/changed/desired column to temporary table. If saved we can fetch it from DB and then it can be used to compare with current column
412+
public function tmpSaveNewCol(string $tableAlias, \cebe\yii2openapi\db\ColumnSchema $columnSchema): \yii\db\ColumnSchema
424413
{
425-
$tableName = 'tmp_table_';
414+
$tmpTableName = 'tmp_table_';
415+
$tmpEnumName = function (string $columnName): string {
416+
return '"tmp_enum_'.$columnName.'_"';
417+
};
418+
$rawTableName = $this->db->schema->getRawTableName($tableAlias);
419+
$innerEnumTypeName = "\"enum_{$tmpTableName}_{$columnSchema->name}\"";
426420

427-
Yii::$app->db->createCommand('DROP TABLE IF EXISTS '.$tableName)->execute();
421+
Yii::$app->db->createCommand('DROP TABLE IF EXISTS '.$tmpTableName)->execute();
428422

429423
if (is_string($columnSchema->xDbType) && !empty($columnSchema->xDbType)) {
430-
$column = [$columnSchema->name.' '.$this->newColStr($columnSchema)];
424+
$name = MigrationRecordBuilder::quote($columnSchema->name);
425+
$column = [$name.' '.$this->newColStr($tmpTableName, $columnSchema)];
426+
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {
427+
$column = strtr($column, [$innerEnumTypeName => $tmpEnumName($columnSchema->name)]);
428+
}
431429
} else {
432-
$column = [$columnSchema->name => $this->newColStr($columnSchema)];
430+
$column = [$columnSchema->name => $this->newColStr($tmpTableName, $columnSchema)];
431+
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {
432+
$column[$columnSchema->name] = strtr($column[$columnSchema->name], [$innerEnumTypeName => $tmpEnumName($columnSchema->name)]);
433+
}
433434
}
434435

435-
Yii::$app->db->createCommand()->createTable($tableName, $column)->execute();
436+
// create enum if relevant
437+
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {
438+
$allEnumValues = $columnSchema->enumValues;
439+
$allEnumValues = array_map(function ($aValue) {
440+
return "'$aValue'";
441+
}, $allEnumValues);
442+
Yii::$app->db->createCommand(
443+
'CREATE TYPE '.$tmpEnumName($columnSchema->name).' AS ENUM('.implode(', ', $allEnumValues).')'
444+
)->execute();
445+
}
446+
447+
Yii::$app->db->createCommand()->createTable($tmpTableName, $column)->execute();
448+
449+
$table = Yii::$app->db->getTableSchema($tmpTableName);
436450

437-
$table = Yii::$app->db->getTableSchema($tableName);
451+
Yii::$app->db->createCommand()->dropTable($tmpTableName)->execute();
438452

439-
Yii::$app->db->createCommand()->dropTable($tableName)->execute();
453+
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {// drop enum
454+
Yii::$app->db->createCommand('DROP TYPE '.$tmpEnumName($columnSchema->name))->execute();
455+
if ('"'.$table->columns[$columnSchema->name]->dbType.'"' !== $tmpEnumName($columnSchema->name)) {
456+
throw new \Exception('Unknown error related to PgSQL enum '.$table->columns[$columnSchema->name]->dbType);
457+
}
458+
// reset back column enum name to original as we are comparing with current
459+
// 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
460+
$table->columns[$columnSchema->name]->dbType = 'enum_'.$rawTableName.'_'.$columnSchema->name;
461+
}
440462

441463
return $table->columns[$columnSchema->name];
442464
}
443465

444-
public function newColStr(\cebe\yii2openapi\db\ColumnSchema $columnSchema): string
466+
public function newColStr(string $tableAlias, \cebe\yii2openapi\db\ColumnSchema $columnSchema): string
445467
{
446-
$ctc = new ColumnToCode(\Yii::$app->db->schema, $columnSchema, false, false, true);
468+
$ctc = new ColumnToCode(\Yii::$app->db->schema, $tableAlias, $columnSchema, false, false, true);
447469
return ColumnToCode::undoEscapeQuotes($ctc->getCode());
448470
}
471+
472+
public static function isEnum(\yii\db\ColumnSchema $columnSchema): bool
473+
{
474+
if (!empty($columnSchema->enumValues) && is_array($columnSchema->enumValues)) {
475+
return true;
476+
}
477+
return false;
478+
}
479+
480+
public static function isEnumValuesChanged(
481+
\yii\db\ColumnSchema $current,
482+
\yii\db\ColumnSchema $desired
483+
): bool {
484+
if (static::isEnum($current) && static::isEnum($desired) &&
485+
$current->enumValues !== $desired->enumValues) {
486+
return true;
487+
}
488+
return false;
489+
}
490+
491+
public function isDefaultValueChanged(
492+
ColumnSchema $current,
493+
ColumnSchema $desired
494+
): bool {
495+
// if the default value is object of \yii\db\Expression then default value is expression instead of constant. See https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
496+
// in such case instead of comparing two objects, we should compare expression
497+
498+
if ($current->defaultValue instanceof Expression &&
499+
$desired->defaultValue instanceof Expression
500+
&& $current->defaultValue->expression === $desired->defaultValue->expression
501+
) {
502+
return false;
503+
}
504+
505+
if ($current->defaultValue !== $desired->defaultValue) {
506+
return true;
507+
}
508+
return false;
509+
}
449510
}

0 commit comments

Comments
 (0)