sqldef + MySQL 8 の環境にて、schema.sql に CHECK (priority IN ('normal')) と書いたところ、2回目以降の実行でも毎回 DROP CHECK / ADD CONSTRAINT の ALTER が出続けるという現象に遭遇しました。

再現

MySQL 8.4.8、mysqldef v3.11.3 で確認しました。

schema.sql:

CREATE TABLE tasks (
  id BIGINT NOT NULL AUTO_INCREMENT,
  priority VARCHAR(50) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT chk_tasks_priority
    CHECK (priority IN ('normal'))
);

これを mysqldef で適用したあと、もう一度同じ schema.sql で実行すると、毎回 ALTER が出続けます。

ALTER TABLE `tasks` DROP CHECK `chk_tasks_priority`;
ALTER TABLE `tasks` ADD CONSTRAINT `chk_tasks_priority` CHECK (priority in ('normal'));

何度実行しても収束せず、CIで「diff なし」を期待していると常に落ちる、という状態になります。

SHOW CREATE TABLE してみる

差分を出されている DB 側の状態が気になり、 SHOW CREATE TABLE で実体を確認してみました。

CREATE TABLE `tasks` (
  ...
  CONSTRAINT `chk_tasks_priority` CHECK ((`priority` = _utf8mb4'normal'))
)

schema.sql に書いた IN ('normal') が、 DB 側では = 'normal' になっています。

原因:MySQL は CHECK 式を正規化して保存する

調べてみると、 MySQL 8 は CHECK 制約の式をパースしたあと、正規化した形で保存するようです。1要素の IN (...) は等価な = に書き換えられます。

sqldef は schema.sql の状態と DB の現状を比較して差分の DDL を生成します。今回のケースでは、

  • schema.sql 側: IN ('normal')
  • DB 側: = 'normal'

が一致しないと判定され、毎回 DROPADD の ALTER が生成されます。ADD 後に DB 側がまた = に正規化されるので、いくら適用しても収束しません。

回避策

CHECK 制約を書くときに、要素数で書き分けます。

-- 1要素なら =
CHECK (priority = 'normal')

-- 2要素以上なら IN
CHECK (priority IN ('normal', 'high'))

= 'normal' で書けば DB 側の表現と一致し、 mysqldefNothing is modified を返して冪等になります。

2要素以上の IN (...) は MySQL も内部表現を IN のまま保存するため、こちらも冪等です。

さいごに

宣言型のスキーマ管理ツールは「目標 = 現状」の同値判定が肝です。その判定は DB エンジン側の式の正規化に左右されるため、 schema.sql に書いた SQL がそのまま DB に保存される保証はありません。

何かおかしいなと思ったら SHOW CREATE TABLE で実体を確認する、というのを安全装置として覚えておこうと思いました。