Skip to content

Timestamp enum x db type bug fix #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
08e30ab
Test
SOHELAHMED7 Dec 21, 2022
e367a47
Merge branch '100-timestamp-migrations-do-not-work-in-mariadb' of git…
SOHELAHMED7 Dec 21, 2022
ec097d7
Test 2
SOHELAHMED7 Dec 21, 2022
03ca98b
test 3
SOHELAHMED7 Dec 21, 2022
6c90780
Merge branch 'master' of https://github.com/SOHELAHMED7/yii2-openapi …
SOHELAHMED7 Dec 21, 2022
984c7b3
Test
SOHELAHMED7 Dec 21, 2022
96257f6
Cleanup
SOHELAHMED7 Dec 21, 2022
d3ecd74
test
SOHELAHMED7 Dec 21, 2022
c809394
Fix issue in setting default expression
SOHELAHMED7 Dec 21, 2022
3d4d71c
Fix issue: decimal considered as string instead of double/float
SOHELAHMED7 Dec 21, 2022
a9c6ec2
Fix enum issue for MySQL, MariaDB and partially for PgSQL #111
SOHELAHMED7 Dec 22, 2022
89f98c2
WIP
SOHELAHMED7 Dec 23, 2022
b047f7d
undo WIP
SOHELAHMED7 Dec 23, 2022
6cea7f2
Fix issue: CREATE/DROP enum in PGSQL up/down migration not in correct…
SOHELAHMED7 Dec 23, 2022
6a9a680
Fix style
SOHELAHMED7 Dec 23, 2022
c60403f
Fix quote issue in migration down code for MySQL and Maria + add more…
SOHELAHMED7 Dec 23, 2022
fcc53a2
Fix multiple issue + add more tests for eunm in Pgsql + other DB
SOHELAHMED7 Dec 23, 2022
130c69a
Fix bug in column change detection in Pgsql
SOHELAHMED7 Dec 24, 2022
1b0bc2e
Fix failing tests
SOHELAHMED7 Dec 24, 2022
a037827
Fix enum related issues + add more tests for it for all 3 DBs
SOHELAHMED7 Dec 24, 2022
dae8e4c
Implement Enum change value migration - WIP
SOHELAHMED7 Dec 24, 2022
e67e8e1
WIP
SOHELAHMED7 Dec 24, 2022
ded2d2b
Complete enum tests + add stub for enum values change test
SOHELAHMED7 Dec 27, 2022
af614eb
Remove unwanted files
SOHELAHMED7 Dec 27, 2022
39d2ff0
Add docs in README
SOHELAHMED7 Dec 27, 2022
5badea4
Restore tests that were deleted in https://github.com/cebe/yii2-opena…
SOHELAHMED7 Dec 27, 2022
72c2a79
Add enh.
SOHELAHMED7 Dec 27, 2022
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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ up:
docker-compose up -d
echo "Waiting for mariadb to start up..."
docker-compose exec -T mysql timeout 60s sh -c "while ! (mysql -udbuser -pdbpass -h maria --execute 'SELECT 1;' > /dev/null 2>&1); do echo -n '.'; sleep 0.1 ; done; echo 'ok'" || (docker-compose ps; docker-compose logs; exit 1)
echo "Create another test DB for PgSQL ..." # created because of enum in PgSQL are different than MySQL
docker-compose exec -T postgres bash -c "psql --username dbuser --dbname testdb -c 'create database pg_test_db_2;'; psql --username dbuser --dbname testdb -c 'grant all privileges on database pg_test_db_2 to dbuser;'" || (docker-compose ps; docker-compose logs; exit 1)


cli:
docker-compose exec php bash
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ e.g. attribute = 'my_property'.
nullable: false
```

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

```yaml
test_table:
Expand All @@ -329,6 +329,8 @@ It work on MariaDb.
- three
```

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.

### Handling of `numeric` (#numeric, #MariaDb)
precision-default = 10
scale-default = 2
Expand Down Expand Up @@ -372,9 +374,10 @@ Generated files:

# Development

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` .
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` .

```bash
cd tests
./yii migrate-mysql/up
./yii migrate-mysql/down 4

Expand Down
20 changes: 12 additions & 8 deletions src/lib/ColumnToCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ public function getCode(bool $quoted = false):string
}

$code = $this->rawParts['type'] . ' ' . $this->rawParts['nullable'] . $default;
if (ApiGenerator::isMysql() && $this->isEnum()) {
return $quoted ? '"' . str_replace("\'", "'", $code) . '"' : $code;
if ((ApiGenerator::isMysql() || ApiGenerator::isMariaDb()) && $this->isEnum()) {
return $quoted ? "'" . $code . "'" : $code;
}
if (ApiGenerator::isPostgres() && $this->alterByXDbType) {
return $quoted ? "'" . $this->rawParts['type'] . "'" : $this->rawParts['type'];
Expand All @@ -160,7 +160,7 @@ public function getCode(bool $quoted = false):string
public function getAlterExpression(bool $addUsingExpression = false):string
{
if ($this->isEnum() && ApiGenerator::isPostgres()) {
return "'" . sprintf('enum_%1$s USING %1$s::enum_%1$s', $this->column->name) . "'";
return "'" . sprintf('enum_%1$s USING "%1$s"::enum_%1$s', $this->column->name) . "'";
}
if ($this->column->dbType === 'tsvector') {
return "'" . $this->rawParts['type'] . "'";
Expand Down Expand Up @@ -270,7 +270,9 @@ public static function enumToString(array $enum):string

public static function mysqlEnumToString(array $enum):string
{
return implode(', ', array_map('self::wrapQuotes', $enum));
return implode(', ', array_map(function ($aEnumValue) {
return self::wrapQuotes($aEnumValue, '"');
}, $enum));
}

private function defaultValueJson(array $value):string
Expand Down Expand Up @@ -329,7 +331,7 @@ private function resolve():void
$this->rawParts['type'] =
$this->column->dbType . (strpos($this->column->dbType, '(') !== false ? '' : $rawSize);
}

$this->isBuiltinType = $this->raw ? false : $this->getIsBuiltinType($type, $dbType);

$this->resolveDefaultValue();
Expand All @@ -346,7 +348,7 @@ private function getIsBuiltinType($type, $dbType)
return false;
}

if ($this->isEnum() && ApiGenerator::isMariaDb()) {
if ($this->isEnum()) {
return false;
}
if ($this->fromDb === true) {
Expand Down Expand Up @@ -421,16 +423,18 @@ private function resolveDefaultValue():void
break;
default:
$isExpression = StringHelper::startsWith($value, 'CURRENT')
|| StringHelper::startsWith($value, 'current')
|| StringHelper::startsWith($value, 'LOCAL')
|| substr($value, -1, 1) === ')';
if ($isExpression) {
$this->fluentParts['default'] = 'defaultExpression("' . self::escapeQuotes((string)$value) . '")';
$this->rawParts['default'] = $value;
} else {
$this->fluentParts['default'] = $expectInteger
? 'defaultValue(' . $value . ')' : 'defaultValue("' . self::escapeQuotes((string)$value) . '")';
$this->rawParts['default'] = $expectInteger ? $value : self::wrapQuotes($value);
}
$this->rawParts['default'] = $expectInteger ? $value : self::wrapQuotes($value);
if (ApiGenerator::isMysql() && $this->isEnum()) {
if ((ApiGenerator::isMysql() || ApiGenerator::isMariaDb()) && $this->isEnum()) {
$this->rawParts['default'] = self::escapeQuotes($this->rawParts['default']);
}
}
Expand Down
55 changes: 43 additions & 12 deletions src/lib/migrations/BaseMigrationBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace cebe\yii2openapi\lib\migrations;

use cebe\yii2openapi\generator\ApiGenerator;
use cebe\yii2openapi\lib\ColumnToCode;
use cebe\yii2openapi\lib\items\DbModel;
use cebe\yii2openapi\lib\items\ManyToManyRelation;
Expand Down Expand Up @@ -203,14 +204,6 @@ function (string $unknownColumn) {
// do not adjust existing primary keys
continue;
}
if (!empty($current->enumValues)) {
$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;
Expand Down Expand Up @@ -423,20 +416,39 @@ protected function isNeedUsingExpression(string $fromType, string $toType):bool
public function tmpSaveNewCol(\cebe\yii2openapi\db\ColumnSchema $columnSchema): \yii\db\ColumnSchema
{
$tableName = 'tmp_table_';
$db = 'db';
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {
$db = 'pg_test_db_2';
}

Yii::$app->db->createCommand('DROP TABLE IF EXISTS '.$tableName)->execute();
Yii::$app->{$db}->createCommand('DROP TABLE IF EXISTS '.$tableName)->execute();

if (is_string($columnSchema->xDbType) && !empty($columnSchema->xDbType)) {
$column = [$columnSchema->name.' '.$this->newColStr($columnSchema)];
} else {
$column = [$columnSchema->name => $this->newColStr($columnSchema)];
}

Yii::$app->db->createCommand()->createTable($tableName, $column)->execute();
// create enum if relevant
if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {
$allEnumValues = $columnSchema->enumValues;
$allEnumValues = array_map(function ($aValue) {
return "'$aValue'";
}, $allEnumValues);
Yii::$app->{$db}->createCommand(
'CREATE TYPE enum_'.$columnSchema->name.' AS ENUM('.implode(', ', $allEnumValues).')'
)->execute();
}

$table = Yii::$app->db->getTableSchema($tableName);
Yii::$app->{$db}->createCommand()->createTable($tableName, $column)->execute();

Yii::$app->db->createCommand()->dropTable($tableName)->execute();
$table = Yii::$app->{$db}->getTableSchema($tableName);

Yii::$app->{$db}->createCommand()->dropTable($tableName)->execute();

if (ApiGenerator::isPostgres() && static::isEnum($columnSchema)) {// drop enum
Yii::$app->{$db}->createCommand('DROP TYPE enum_'.$columnSchema->name)->execute();
}

return $table->columns[$columnSchema->name];
}
Expand All @@ -446,4 +458,23 @@ public function newColStr(\cebe\yii2openapi\db\ColumnSchema $columnSchema): stri
$ctc = new ColumnToCode(\Yii::$app->db->schema, $columnSchema, false, false, true);
return ColumnToCode::undoEscapeQuotes($ctc->getCode());
}

public static function isEnum(\yii\db\ColumnSchema $columnSchema): bool
{
if (!empty($columnSchema->enumValues) && is_array($columnSchema->enumValues)) {
return true;
}
return false;
}

public static function isEnumValuesChanged(
\yii\db\ColumnSchema $current,
\yii\db\ColumnSchema $desired
): bool {
if (static::isEnum($current) && static::isEnum($desired) &&
$current->enumValues !== $desired->enumValues) {
return true;
}
return false;
}
}
4 changes: 2 additions & 2 deletions src/lib/migrations/MysqlMigrationBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir
foreach ($changed as $attr) {
$newColumn->$attr = $desired->$attr;
}
if (!empty($newColumn->enumValues)) {
$newColumn->dbType = 'enum';
if (static::isEnum($newColumn)) {
$newColumn->dbType = 'enum'; // TODO this is concretely not correct
}
$this->migration->addUpCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $newColumn))
->addDownCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $current));
Expand Down
39 changes: 22 additions & 17 deletions src/lib/migrations/PostgresMigrationBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ protected function buildColumnsCreation(array $columns):void
{
foreach ($columns as $column) {
$tableName = $this->model->getTableAlias();
if ($column->dbType === 'enum') {
if (static::isEnum($column)) {
$this->migration->addUpCode($this->recordBuilder->createEnum($column->name, $column->enumValues))
->addDownCode($this->recordBuilder->dropEnum($column->name));
->addDownCode($this->recordBuilder->dropEnum($column->name), true);
}
$this->migration->addUpCode($this->recordBuilder->addColumn($tableName, $column))
->addDownCode($this->recordBuilder->dropColumn($tableName, $column->name));
Expand All @@ -41,9 +41,9 @@ protected function buildColumnsDrop(array $columns):void
$tableName = $this->model->getTableAlias();
$this->migration->addDownCode($this->recordBuilder->addDbColumn($tableName, $column))
->addUpCode($this->recordBuilder->dropColumn($tableName, $column->name));
if ($column->dbType === 'enum') {
if (static::isEnum($column)) {
$this->migration->addDownCode($this->recordBuilder->createEnum($column->name, $column->enumValues))
->addUpCode($this->recordBuilder->dropEnum($column->name), true);
->addUpCode($this->recordBuilder->dropEnum($column->name));
}
}
}
Expand All @@ -54,22 +54,20 @@ protected function buildColumnsDrop(array $columns):void
protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desired, array $changed):void
{
$tableName = $this->model->getTableAlias();
$isChangeToEnum = $current->type !== $desired->type && !empty($desired->enumValues);
$isChangeFromEnum = $current->type !== $desired->type && !empty($current->enumValues);
$isChangedEnum = $current->type === $desired->type && !empty($current->enumValues);
$isChangeToEnum = !static::isEnum($current) && static::isEnum($desired);
$isChangeFromEnum = static::isEnum($current) && !static::isEnum($desired);
$isChangedEnum = static::isEnumValuesChanged($current, $desired);
if ($isChangedEnum) {
// Generation for change enum values not supported. Do it manually
// This action require several steps and can't be applied during single transaction
return;
}
if ($isChangeToEnum) {
$this->migration->addUpCode($this->recordBuilder->createEnum($desired->name, $desired->enumValues), true);
}
if ($isChangeFromEnum) {
$this->migration->addUpCode($this->recordBuilder->dropEnum($current->name));
}
if (!empty(array_intersect(['type', 'size'], $changed))) {
$addUsing = $this->isNeedUsingExpression($desired->type, $current->type);

if (!empty(array_intersect(['type', 'size'
, 'dbType', 'phpType'
, 'precision', 'scale', 'unsigned'
], $changed))) {
$addUsing = $this->isNeedUsingExpression($current->type, $desired->type);
$this->migration->addUpCode($this->recordBuilder->alterColumnType($tableName, $desired));
$this->migration->addDownCode($this->recordBuilder->alterColumnTypeFromDb($tableName, $current, $addUsing));
}
Expand All @@ -93,9 +91,16 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir
$this->migration->addUpCode($upCode)->addDownCode($downCode, true);
}
}
if ($isChangeToEnum) {
$this->migration->addUpCode($this->recordBuilder->createEnum($desired->name, $desired->enumValues), true);
}
if ($isChangeFromEnum) {
$this->migration->addUpCode($this->recordBuilder->dropEnum($current->name));
}

if ($isChangeFromEnum) {
$this->migration
->addDownCode($this->recordBuilder->createEnum($current->name, $current->enumValues), true);
->addDownCode($this->recordBuilder->createEnum($current->name, $current->enumValues));
}
if ($isChangeToEnum) {
$this->migration->addDownCode($this->recordBuilder->dropEnum($current->name), true);
Expand Down Expand Up @@ -132,7 +137,7 @@ protected function createEnumMigrations():void
foreach ($enums as $attr) {
$this->migration
->addUpCode($this->recordBuilder->createEnum($attr->columnName, $attr->enumValues), true)
->addDownCode($this->recordBuilder->dropEnum($attr->columnName));
->addDownCode($this->recordBuilder->dropEnum($attr->columnName), true);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/openapi/PropertySchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ public static function findMoreDetailOf(string $xDbType): array
}

/**
* This method is copied from protected method `getColumnPhpType()` of \yii\db\Schema class
* This method is copied + enhanced from protected method `getColumnPhpType()` of \yii\db\Schema class
* Extracts the PHP type from abstract DB type.
* @param \yii\db\ColumnSchema $column the column schema information
* @return string PHP type name
Expand All @@ -546,6 +546,7 @@ public static function getColumnPhpType(ColumnSchema $column): string
YiiDbSchema::TYPE_BOOLEAN => 'boolean',
YiiDbSchema::TYPE_FLOAT => 'double',
YiiDbSchema::TYPE_DOUBLE => 'double',
YiiDbSchema::TYPE_DECIMAL => 'double', # (enhanced)
YiiDbSchema::TYPE_BINARY => 'resource',
YiiDbSchema::TYPE_JSON => 'array',
];
Expand Down
15 changes: 15 additions & 0 deletions tests/DbTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,19 @@ protected function changeDbToPgsql()
self::assertNotInstanceOf(MySqlSchema::class, Yii::$app->db->schema);
self::assertInstanceOf(PgSqlSchema::class, Yii::$app->db->schema);
}

protected function checkFiles(array $actual, array $expected)
{
self::assertEquals(
count($actual),
count($expected)
);
foreach ($actual as $index => $file) {
$expectedFilePath = $expected[$index];
self::assertFileExists($file);
self::assertFileExists($expectedFilePath);

$this->assertFileEquals($expectedFilePath, $file, "Failed asserting that file contents of\n$file\nare equal to file contents of\n$expectedFilePath");
}
}
}
7 changes: 7 additions & 0 deletions tests/config/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
'charset' => 'utf8',
'tablePrefix'=>'itt_',
],
'pg_test_db_2' => [
'class' => \yii\db\Connection::class,
'dsn' => 'pgsql:host=postgres;dbname=pg_test_db_2',
'username' => 'dbuser',
'password' => 'dbpass',
'charset' => 'utf8',
],
'mysql' => [
'class' => \yii\db\Connection::class,
'dsn' => 'mysql:host=mysql;dbname=testdb',
Expand Down
6 changes: 5 additions & 1 deletion tests/fixtures/blog.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
->setSize(200)->setRequired()->setFakerStub('substr($faker->safeEmail, 0, 200)'),
'password' => (new Attribute('password', ['phpType' => 'string', 'dbType' => 'string']))
->setRequired()->setFakerStub('$faker->password'),
'role' => (new Attribute('role', ['phpType' => 'string', 'dbType' => 'string']))
->setSize(20)
->setDefault('reader')
->setFakerStub('$faker->randomElement([\'admin\', \'editor\', \'reader\'])'),
'flags' => (new Attribute('flags', ['phpType'=>'int', 'dbType'=>'integer']))->setDefault(0)->setFakerStub
('$faker->numberBetween(0, 1000000)'),
'created_at' => (new Attribute('created_at', ['phpType' => 'string', 'dbType' => 'datetime']))
Expand All @@ -28,7 +32,7 @@
'indexes' => [
'users_email_key' => DbIndex::make('users', ['email'], null, true),
'users_username_key' => DbIndex::make('users', ['username'], null, true),
'users_flags_index' => DbIndex::make('users', ['flags'])
'users_role_flags_index' => DbIndex::make('users', ['role', 'flags'])
]
]),
'category' => new DbModel([
Expand Down
1 change: 1 addition & 0 deletions tests/migrations/m100000_000000_maria.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function up()
'username' => $this->string(200)->notNull()->unique(),
'email' => $this->string(200)->notNull()->unique(),
'password' => $this->string()->notNull(),
'role' => $this->string(20)->null()->defaultValue('reader'),
'flags' => $this->integer()->null()->defaultValue(0),
'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
]);
Expand Down
1 change: 1 addition & 0 deletions tests/migrations/m100000_000000_mysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function up()
'username' => $this->string(200)->notNull()->unique(),
'email' => $this->string(200)->notNull()->unique(),
'password' => $this->string()->notNull(),
'role' => $this->string(20)->null()->defaultValue('reader'),
'flags' => $this->integer()->null()->defaultValue(0),
'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
]);
Expand Down
Loading