LaravelVapor + ServerlessDB で defaultStringLengthが 191 になる

事象

VARCHAR 系のカラムが、開発環境 では 255 で作成されているのに対し、
本番環境 では 191 で作成されているケースが確認された。

本番環境は、AWS 上の Laravel Vapor + Aurora Serverless v2(Serverless DB)
開発環境は、PC上の php-fpm + MySQL

これらの環境で、文字長を省略したstring列をmigrationする

Schema::table('users', function (Blueprint $table) {
$table->string('name');
});

すると、
開発環境では CREATE TABLE users (name varchar(255)) が実行されるのに対し、
本番環境では CREATE TABLE users (name varchar(191)) が実行される。

結果、200文字程度の値をDBに登録しようとすると、
開発環境では問題なく登録されるのに対し、本番環境では191文字に切り詰められてしまう問題が発生した。

解析

文字長を省略した string列の migration には、$defaultStringLength が利用されるのは
Blueprint#string のソースを見るとすぐわかる。
が、特に$defaultStringLengthに191や255を設定するようなコードを書いた覚えがない。

VAPOR_SERVERLESS_DB が関係あるらしいとの天啓をうけて

echo 'VAPOR_SERVERLESS_DB: ' . env('VAPOR_SERVERLESS_DB', 'not set') . PHP_EOL;
echo 'defaultStringLength: ' . \Illuminate\Database\Schema\Builder::$defaultStringLength . PHP_EOL;

を確認してみたところ、
開発環境では、VAPOR_SERVERLESS_DBは 未定義 , $defaultStringLength= 255
本番環境では、VAPOR_SERVERLESS_DB= 1 , $defaultStringLength= 191
を得た。

どうも核心らしい。が、
Vapor の Environment Variables 画面で VAPOR_SERVERLESS_DB=1 を設定してはいないし、
laravelのソースにも開発ソースにもVAPOR_SERVERLESS_DB は登場しない。

VAPOR_SERVERLESS_DBって?

VAPOR_SERVERLESS_DB は Vapor runtime が Lambda 起動時に「内部的に注入している」隠し環境変数
ざっくり言うとこう動く

Lambda
↓ bootstrap
Vapor runtimeプロセス
↓ Serverless DB の利用を検知し、export VAPOR_SERVERLESS_DB=1
↓ VAPOR_SERVERLESS_DB=1を受けて、Schema::defaultStringLength(191)を含むauto_prepend_fileを差し込む
PHP-FPMプロセス
↓ auto_prepend_file を実行
Laravelプロセス
↓ Laravel bootstrap

LaravelVapor はデプロイの際に、
auto_prepend_file のような仕組みで “パッチコード” を PHP に差し込む。
その中で、 Schema::defaultStringLength(191) といったことをしている。

Laravel の起動前に PHP の実行環境にパッチを注入しているから Laravel 側からは 一切見えない。
export VAPOR_SERVERLESS_DB=1 はVapor runtimeプロセスの中で動的に設定され、
envやEnvironment Variables を上書きしてまうから、LaravelVaporコンソールから介入することもできない。

対策

AppServiceProvider で上書きする のがよさげ。

class AppServiceProvider extends Illuminate\Support\ServiceProvider
{
public function boot(): void
{
Illuminate\Support\Facades\Schema::defaultStringLength(255);
もしくは
Illuminate\Support\Facades\Schema::defaultStringLength(191);
}
}

Laravel のServiceProviderは Vapor runtime のパッチより後に実行されるので、defaultStringLengthを自分の好みに書き戻すことができる。
これで今後のmigrationは本番と開発とで同じ挙動になる。

既にmigrationしてしまったカラムは

select * from information_schema.columns
where column_type = 'varchar(191)'

でリストアップできるけど、一律に users modify name varchar(255); してしまうとNULL制約やコメントが吹っ飛ぶ。
幸い、運用前のシステムだったので 0からmigrationしなおした。

運用中ならこうかしら、、、自己責任でお願いします。
select
concat(
'alter table`', table_name, '` ',
'modify `', column_name, '` varchar(255)',
case when is_nullable = 'NO' then ' not null' else '' end,
case when column_default is not null
then concat(' default ''', column_default, '''')
else ''
end,
case when column_comment <> ''
then concat(' comment ''', column_comment, '''')
else ''
end,
';'
) as alter_sql
from information_schema.columns
where column_type = 'varchar(191)'

なぜ 191 ?

191という数値は MySQL 5.6 / 5.7時代の InnoDB に遡ることができる。
この頃は、デフォルトの InnoDB 設定で191超のstringをmigrationしようとすると、utf8mb4 + indexサイズの制約から
ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes が発生しえた。
Laravel 5.4 ではdefaultStringLength=191 が推奨であった。

MySQL8時代になってこの問題は解消され、話を聞かなくなった。

Aurora Serverless v2 は Aurora Storage を使用しており、これは MySQL 8 時代の InnoDB と互換性を持つが、InnoDB そのものではない。
互換挙動が微妙に揺らぐ可能性を Vapor runtimeは懸案して、安全側のデフォルトとして 191 を強制している

今回、本番環境を255に寄せたが、開発環境を191に寄せるべきということかもしれない。

調査ツール

おまけ
本番環境で変数を抜くのに仕込んだツール。
LaravelVapor コンソールの RUN COMMAND 機能で artisan app:health としてツールを実行する。

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class HealthCheckCommand extends Command
{
protected $signature = 'app:health';
protected $description = 'ヘルスチェック';

public function handle(): void
{
echo 'OK' . PHP_EOL;
echo 'env: ' . config('app.env') . PHP_EOL;
echo "PHP version: " . PHP_VERSION . PHP_EOL;
echo "Laravel version: " . app()->version() . PHP_EOL;

echo "MySQL: ";
try {
$info = DB::selectOne("select version(), @@hostname, @@time_zone");
foreach ($info as $key => $value) {
echo "{$key}={$value} ";
}
echo PHP_EOL;
} catch (\Throwable $e) {
echo $e->getMessage() . PHP_EOL;
}

echo 'VAPOR_SERVERLESS_DB: ' . env('VAPOR_SERVERLESS_DB', 'not set') . PHP_EOL;
echo 'defaultStringLength: ' . \Illuminate\Database\Schema\Builder::$defaultStringLength . PHP_EOL;
}
}
jo

jo

日本さかな検定2級。福岡検定にチャレンジしたい