Osm Admin: `string` Property Migrations. Migration Log
2 years ago ∙ 4 minutes read
Yesterday:
- I finished implementing
string
property migrations. - Migrations have got a meaningful log explaining what migrations take place, and why.
- Along the way, I fixed numerous issues.
In detail:
- Section IDs In Blog Posts
string
Property Migrations- Migration Log
- Fixing
nullable
Andauto_increment
Issues
Section IDs In Blog Posts
One of the objectives of this blog (see its source code) that it should be equally browsable both in a Web browser and on GitHub. For this reason, all internal links are written in a GitHub way, and during rendering they are converted to absolute links that works in a browser.
However, I've noticed that section IDs are not fully compatible with GitHub. For example, for the Migration\Int_::size()
heading, the blog generates #migration-int_-size
while GitHub expects #migrationint_size
.
Let's fix that.
The faulty code:
// Osm\Data\Markdown\File
protected function generateId(string $heading): string {
$id = mb_strtolower($heading);
$id = preg_replace('/[^\w\d\- ]+/u', ' ', $id);
$id = preg_replace('/\s+/u', '-', $id);
$id = preg_replace('/^\-+/u', '', $id);
$id = preg_replace('/\-+$/u', '', $id);
return $id;
}
Here is the fix:
protected function generateId(string $heading): string {
$id = mb_strtolower($heading);
$id = preg_replace('/[^\w\d\s]+/u', '', $id);
$id = preg_replace('/\s+/u', '-', $id);
$id = preg_replace('/^\-+/u', '', $id);
$id = preg_replace('/\-+$/u', '', $id);
return $id;
}
string
Property Migrations
Migration\String_::size()
In a sense, String_::$size
is similar to Int_::$size
.
If TEXT
column becomes TINYTEXT
, all values longer than maximum allowed length (for TINYTEXT
, it's 85
characters) should be shortened.
On the contrary, if the maximum length increases, no shortening is necessary.
If conversion is needed, pre-alter phase should not change the column definition.
Notice. If #[Length]
attribute is used, VARCHAR
is used instead, and all of this TEXT
logic is not applied.
It should be possible to invoke text shortening multiple times as it's needed in other cases, for example, if max_length
changes.
The implementation:
protected function size(): void {
if ($this->property->new->max_length) {
return;
}
if (!$this->property->old) {
$this->preSize();
return;
}
if ($this->property->old->size === $this->property->new->size) {
return;
}
if ($this->becomingShorter()) {
$this->truncate();
$this->postSize();
}
else {
$this->preSize();
}
}
protected function preSize(): void {
if ($this->mode == Property::CREATE ||
$this->mode == Property::PRE_ALTER)
{
$this->setSize();
}
}
protected function postSize(): void {
if ($this->mode == Property::POST_ALTER) {
$this->setSize();
}
}
protected function setSize(): void {
if ($this->column) {
$this->column->type(
$this->sizes[$this->property->new->size]->sql_type);
$this->run = true;
}
}
protected function becomingShorter(): bool {
return $this->maxLength($this->property->old) >
$this->maxLength($this->property->new);
}
protected function maxLength(
PropertyObject\String_|\stdClass|null $property): int
{
if (!$property) {
return 0;
}
if ($property->max_length) {
return $property->max_length;
}
return $this->sizes[$property->size]->max_length;
}
protected function truncate(): void {
if ($this->truncate) {
return;
}
$this->truncate = true;
if ($this->mode === Property::CONVERT) {
$maxLength = $this->maxLength($this->property->new);
$this->new_value =
"IF(LENGTH({$this->new_value} ?? '') > $maxLength, " .
"LEFT({$this->new_value}, $maxLength), {$this->new_value})";
$this->run = true;
}
}
Migration\String_::length()
Handling of String_::$max_length
is similar to String_::$size
:
protected function length(): void {
if (!$this->property->new->max_length) {
return;
}
if (!$this->property->old) {
$this->preLength();
return;
}
if ($this->property->old->max_length ===
$this->property->new->max_length)
{
return;
}
if ($this->becomingShorter()) {
$this->truncate();
$this->postLength();
}
else {
$this->preLength();
}
}
protected function preLength(): void {
if ($this->mode == Property::CREATE ||
$this->mode == Property::PRE_ALTER)
{
$this->setSize();
}
}
protected function postLength(): void {
if ($this->mode == Property::POST_ALTER) {
$this->setLength();
}
}
protected function setLength(): void {
if ($this->column) {
$this->column->type('string');
/** @noinspection PhpUndefinedMethodInspection */
$this->column->length($this->property->new->max_length);
$this->run = true;
}
}
Migration Log
After writing lots of untested code, it's time to test it, and to do it effectively, I need the migration log that explains what migrations take place, and why.
Created/Altered Columns
Adding logging:
// Osm\Admin\Schema\Diff\Migration::init()
if ($this->table) {
if ($this->property->old) {
$this->logProperty(__("Altering property ':property'", [
'property' => $this->property->new->name,
]));
}
else {
$this->logProperty(__("Creating property ':property'", [
'property' => $this->property->new->name,
]));
}
}
Here is the output:
Migrating 'Osm\Admin\Samples\Migrations\String_\V001\' schema fixture
Creating 'products' table
Creating property 'id'
Creating property 'title'
Creating system columns: '_data', `_overrides`
---------------------------------------------
Migrating 'Osm\Admin\Samples\Migrations\String_\V002\' schema fixture
Pre-altering 'products' table
Altering property 'id'
Altering property 'title'
Creating property 'description'
It's interesting why id
column is being altered - it shouldn't. Logging column attributes should help.
Column Attributes
Adding logging:
protected function explicit(): void {
if ($this->table) {
$this->logAttribute('explicit');
}
...
}
protected function logAttribute(string $attr): void {
if ($this->property->old) {
if ($this->property->old->$attr ?? null ===
$this->property->new->$attr)
{
return;
}
$message = __(":attribute: :old => :new", [
'attribute' => $attr,
'old' => var_export($this->property->old->$attr ?? null,
true),
'new' => var_export($this->property->old->$attr, true),
]);
}
else {
$message = __(":attribute: :new", [
'attribute' => $attr,
'new' => var_export($this->property->new->$attr, true),
]);
}
$this->log->notice(' ' . $message);
}
Output:
Migrating 'Osm\Admin\Samples\Migrations\String_\V001\' schema fixture
Creating 'products' table
Creating property 'id'
explicit: true
type: 'int'
nullable: false
size: 'medium'
unsigned: true
auto_increment: true
Creating property 'title'
explicit: false
type: 'string'
nullable: true
size: 'small'
max_length: NULL
Creating system columns: '_data', `_overrides`
---------------------------------------------
Migrating 'Osm\Admin\Samples\Migrations\String_\V002\' schema fixture
Pre-altering 'products' table
Altering property 'id'
Altering property 'title'
Creating property 'description'
explicit: true
type: 'string'
nullable: true
size: 'small'
max_length: NULL
The log still doesn't answer why id
is being altered.
$this->run = true
To catch source lines triggering the column change, I replaced all $this->run = true
lines with calls to new run()
method:
protected function run(string $attr): void {
$this->run = true;
if ($this->table && $this->property->old) {
$this->log->notice(" !{$attr}");
}
}
And now I see what attributes trigger the column change:
Migrating 'Osm\Admin\Samples\Migrations\String_\V002\' schema fixture
Pre-altering 'products' table
Altering property 'id'
!nullable
!auto_increment
Altering property 'title'
!nullable [] []
Creating property 'description'
...
Fixing nullable
And auto_increment
Issues
Now, there are two issues with nullable
:
- When changing
unsigned int
column, it becomessigned
. - It triggers unnecessary column change.
Unintended Unsigned => Signed
Fix:
protected function unsigned(): void {
...
$this->preOldUnsigned();
...
}
protected function preOldUnsigned(): void {
if ($this->mode == Property::CREATE ||
$this->mode == Property::PRE_ALTER)
{
if ($this->column && $this->property->old->explicit) {
if ($this->property->old->actually_unsigned) {
$this->column->unsigned();
}
}
}
}
Later. Maybe other column attributes should also be set in the pre-alter phase.
Unnecessary Column Change
To fix it, I wrapped run()
into an additional check if the nullable
attribute has actually changed:
if ($this->property->new->actually_nullable !==
$this->property->old?->actually_nullable)
{
$this->run('nullable');
}