🔥

CodeIgniter4 PostgreSQL で modifyColumn 実行時、常に NOT NULL 制約が追加される件への暫定対応

2023/02/15に公開
2

実行環境

  • CodeIgniter 4.2.12
  • PostgreSQL 9.2

再現する手順

<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateTableTestTable extends Migration
{
    public function up()
    {
        $this->forge->addField(
            [
                'col1' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
                'col2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
                'col3' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
            ]
        );
        $this->forge->createTable('test_table');
    }

    public function down()
    {
        $this->forge->dropTable('test_table');
    }
}
<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class ModifyColumnToTestTable extends Migration
{
    public function up()
    {
        $this->forge->modifyColumn('test_table', [
            'col1' => ['type' => 'VARCHAR', 'constraint' => 1],
            'col2' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => true],
            'col3' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => false],
        ]);
    }

    public function down()
    {
    }
}
php spark migrate

以下の SQL が実行される。

CREATE TABLE "test_table"
(
    "col1" VARCHAR(255) NULL,
    "col2" VARCHAR(255) NULL,
    "col3" VARCHAR(255) NULL
);
ALTER TABLE "test_table" ALTER COLUMN "col1" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col1" SET NOT NULL;
ALTER TABLE "test_table" ALTER COLUMN "col2" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col2" SET NOT NULL;
ALTER TABLE "test_table" ALTER COLUMN "col3" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col3" SET NOT NULL;

当該テーブルの最終 DDL は以下となる。

CREATE TABLE "test_table"
(
    "col1" VARCHAR(1) NOT NULL,
    "col2" VARCHAR(1) NOT NULL,
    "col3" VARCHAR(1) NOT NULL
);

問題箇所

PostgreSQL 用の Forge クラス _alterTable メソッド内 ALTER TABLE コマンド組立に
不具合がある。

vendor\codeigniter4\framework\system\Database\Postgre\Forge.php
<?php
// ...
class Forge extends BaseForge
{
    // ...
    protected function _alterTable(string $alterType, string $table, $field)
    {
        if (in_array($alterType, ['DROP', 'ADD'], true)) {
            return parent::_alterTable($alterType, $table, $field);
        }

        $sql  = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
        $sqls = [];

        foreach ($field as $data) {
            if ($data['_literal'] !== false) {
                return false;
            }

            if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . " TYPE {$data['type']}{$data['length']}";
            }

            if (! empty($data['default'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . " SET DEFAULT {$data['default']}";
            }

            if (isset($data['null'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
            }

            if (! empty($data['new_name'])) {
                $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . ' TO ' . $this->db->escapeIdentifiers($data['new_name']);
            }

            if (! empty($data['comment'])) {
                $sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
                    . '.' . $this->db->escapeIdentifiers($data['name'])
                    . " IS {$data['comment']}";
            }
        }

        return $sqls;
    }
    // ...
}

暫定対応

CodeIgniter4 独自ドライバーによるデータベースクラスの拡張 で作成した独自ドライバー内で
メソッド _alterTable をオーバーライドして対応する。

app\Database\MyDriver\Postgre\Forge.php
<?php

namespace App\Database\MyDriver\Postgre;

class Forge extends \CodeIgniter\Database\Postgre\Forge
{
    protected function _alterTable(string $alterType, string $table, $field)
    {
        if (in_array($alterType, ['DROP', 'ADD'], true)) {
            return parent::_alterTable($alterType, $table, $field);
        }

        $sql  = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
        $sqls = [];

        foreach ($field as $data) {
            if ($data['_literal'] !== false) {
                return false;
            }

            if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . " TYPE {$data['type']}{$data['length']}";
            }

            if (! empty($data['default'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . " SET DEFAULT {$data['default']}";
            }

            if (isset($data['null'])) {
                $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
-                   . ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
+                   . ($data['null'] === ' NULL' ? ' DROP' : ' SET') . ' NOT NULL';
            }

            if (! empty($data['new_name'])) {
                $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
                    . ' TO ' . $this->db->escapeIdentifiers($data['new_name']);
            }

            if (! empty($data['comment'])) {
                $sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
                    . '.' . $this->db->escapeIdentifiers($data['name'])
                    . " IS {$data['comment']}";
            }
        }

        return $sqls;
    }
}

動作確認

php spark migrate

以下の SQL が実行される。

CREATE TABLE "test_table"
(
    "col1" VARCHAR(255) NULL,
    "col2" VARCHAR(255) NULL,
    "col3" VARCHAR(255) NULL
);
ALTER TABLE "test_table" ALTER COLUMN "col1" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col1" SET NOT NULL;
ALTER TABLE "test_table" ALTER COLUMN "col2" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col2" DROP NOT NULL;
ALTER TABLE "test_table" ALTER COLUMN "col3" TYPE VARCHAR(1);
ALTER TABLE "test_table" ALTER COLUMN "col3" SET NOT NULL;

当該テーブルの最終 DDL は以下となる。

CREATE TABLE "test_table"
(
    "col1" VARCHAR(1) NOT NULL,
    "col2" VARCHAR(1),
    "col3" VARCHAR(1) NOT NULL
);

おわりに

本来は公式へバグ報告後、バージョンアップを適用すべきだが
開発の都合上、現在最新の v4.3 系へのアップデートが困難な為、力業での対応を行った。

Discussion