MariaDB 10.6 と 10.10 で TIMESTAMP カラムの暗黙付与を検証する

MariaDB では explicit_defaults_for_timestamp が未設定の場合、テーブル内で最初のTIMESTAMP 型カラムに NULL 属性・DEFAULT 句・ON UPDATE 句のいずれも指定していないと、DEFAULT current_timestamp()ON UPDATE current_timestamp() が暗黙に付与されます。

検証環境 mariadb10.6とmariadb10.10

Docker で MariaDB 10.6 と 10.10 のコンテナを構築しました。

services:
  mariadb-10.6:
    image: mariadb:10.6
    ports:
      - "3306:3306"
  mariadb-10.10:
    image: mariadb:10.10
    ports:
      - "3307:3306"

実行したSQL

両バージョンで同じテーブルを作成しました。token_expires カラムには NULL 属性・DEFAULT 句・ON UPDATE 句のいずれも指定していません。

CREATE TABLE timestamp_auto_default_test (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  token_expires TIMESTAMP NOT NULL
);

SHOW CREATE TABLE timestamp_auto_default_test;

検証結果

MariaDB 10.6 では、token_expires カラムに DEFAULT current_timestamp()ON UPDATE current_timestamp() が付与されました。

`token_expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()

MariaDB 10.10 では、同じ DDL でも付与されませんでした。

`token_expires` timestamp NOT NULL

まとめ

暗黙付与の挙動差を確認できました。

原因は、10.10 でこの機能自体が撤廃されたことではありません。explicit_defaults_for_timestampの値を確認したところ、明示的に設定していないにもかかわらず 10.6 では 0、10.10 では 1 でした。
このシステム変数のデフォルト値が 10.10 で変更されたことが、暗黙付与の有無を分けています。

LaravelのマイグレーションでTIMESTAMP型カラムが意図せず自動更新される

発生した問題

Laravelのマイグレーションで作成したTIMESTAMP型カラムが、明示的に値を指定していないUPDATE実行時に自動更新されるという事象に遭遇しました。原因を調査したので、その内容を共有します。

環境

  • PHP(Laravel)
  • MariaDB 10.10未満

再現コード

以下のマイグレーションでカラムを作成します。

Schema::create('your_table', function (Blueprint $table) {
    $table->id();
    $table->timestamp('token_expires');
});

このテーブルに対してレコードをUPDATEすると、token_expiresを指定していないにもかかわらず値が更新されます。

原因

MariaDBの自動付与仕様

MariaDB 10.10未満では、システム変数explicit_defaults_for_timestampが未設定の場合(これがデフォルト値)、テーブル内で最初に定義されたTIMESTAMP型カラムに対し、NULL属性、DEFAULT句、ON UPDATE句のいずれも明示していなければ、次の属性が自動的に付与されます。

  • DEFAULT current_timestamp()
  • ON UPDATE current_timestamp()

この仕様には、押さえておくべきポイントが2つあります。

  1. 対象になるのは最初のTIMESTAMP型カラムのみです。2番目以降のTIMESTAMP型カラムは、DEFAULT句を省略していても自動付与の対象にはなりません。
  2. 「肩代わり」は発生しません。先頭のTIMESTAMP型カラムが何らかの理由(NULL属性の明示など)で対象外だった場合でも、その特別扱いが次のカラムに移ることはありません。

今回のケースでは、token_expiresがテーブル内で最初に定義されたTIMESTAMP型カラムだったため、この自動付与の対象になったと考えられます。

なお、$table->timestamps()created_at/updated_atを先に定義していれば、話は変わります。Laravelはこれらをnullable()付き、つまりNULL属性を明示した状態で生成するため、自動付与の対象から除外され、後続のカラムにも影響しません。

Laravel側の挙動

$table->timestamp('token_expires')と書いた場合、Laravelはnullable()を指定しない限りNOT NULLを付与しますが、DEFAULT句については何も出力しません。

結果として、MariaDBに送られるSQLは次のようになります。

CREATE TABLE `your_table` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `token_expires` TIMESTAMP NOT NULL,
  PRIMARY KEY (`id`)
);

NOT NULLは指定されていても、DEFAULT句、ON UPDATE句が明示されていないため、token_expiresが最初のTIMESTAMP型カラムであるこのテーブルでは、MariaDB(10.10未満)側がDEFAULT current_timestamp() ON UPDATE current_timestamp()を自動で埋め込んでしまいます。

まとめ

  • MariaDB 10.10未満は、explicit_defaults_for_timestampが未設定だと、テーブル内最初のTIMESTAMP型カラムにDEFAULT/ON UPDATE句を自動付与する仕様を持っています。
  • 対象になるのは先頭のTIMESTAMP型カラムのみで、対象外になった分が後続カラムに引き継がれることはありません。
  • Laravelの$table->timestamp()はDEFAULT句を出力しないため、意図せずこの自動付与仕様の対象になり得ます。
  • $table->timestamps()created_at/updated_atを先に定義しておくと、これらがNULL許容で生成されるため、後続のTIMESTAMP型カラムを自動付与仕様から守ることができます。

参考

Md2Backlogを更新しました。

Md2Backlogを更新しました。
Backlog記法へ変換する際のエスケープ処理に不具合があり、修正しました。
特殊文字が正しくエスケープされ、誤変換されなくなりました。

Laravel 11へのバージョンアップでマイグレーションが失敗する

Laravel 11で、カラム変更時のchange()メソッドの挙動が変わりました。

既存のマイグレーションはエラーなく通ってしまうため、意図しないスキーマが気づかないうちに作られるのが厄介です。

変更点

change()は、定義に明示した属性のみを適用し、省略した属性は削除するようになりました。

維持したい属性はすべて書き直す必要があります。

// Laravel 11以降:維持する属性をすべて明示する
$table->integer('votes')->unsigned()->default(1)->comment('my comment')->change();

unsigned()default(1)を省けば、その属性は失われます。

背景

Laravel 10以前はdoctrine/dbalに依存し、変更時に既存のカラム定義をDBから読み取って差分を適用していました。
省略した属性が維持されて見えたのはこのためです。

Laravel 11ではdoctrine/dbal依存が廃止され、ネイティブのスキーマ操作に移行しました。
change()は与えられた定義をそのまま新しい状態として適用するため、明示しない属性は引き継がれません。

実害が出やすいケース

旧挙動を前提に書かれたchange()を、Laravel 11環境でロールバック・再適用するケースです。

// 旧挙動では unsigned / default が維持される前提
$table->integer('votes')->change();

これを再適用すると属性が欠落し、意図と異なるスキーマになります。

マイグレーション自体は成功扱いで終わるため、気づきにくいのが問題です。

バージョンアップ時は、既存のchange()呼び出しを洗い出し、維持すべき属性が明示されているか確認してください。

参考: Laravel 11 マイグレーションドキュメント