SQLiteの文字列一括置換でDBが壊れた話——保護トークン設計の落とし穴
個人開発サービス CheckMe の開発中に発生したインシデント記録です。同じ轍を踏まないよう公開します。
何が起きたか
深夜、SQLiteのレコード50件以上に対して「旧メンバー名→新名称」の一括置換スクリプトを実行した。二重置換を防ぐための「保護トークン方式」で実装したつもりが、トークン文字列に検索パターンの部分文字列が含まれていた。
バグの原因:トークンに検索対象が含まれていた
# NG: 'UIUXD' の中に 'UIUX' が含まれている
text = text.replace('UIUXトップディレクター', 'UIUXD') # 保護
text = text.replace('UIUX', 'UIUXトップディレクター') # ここでトークン内の UIUX にもヒット!
text = text.replace('UIUXD', 'UIUXトップディレクター') # 復元できない
結果として本番DBが以下の状態に:
'テックリードエンジニアエンジニア' # 二重置換
'トップトップマーケッターッター' # 三重置換
'UIUXトップディレクターD' # 未復元の残骸
正しい実装:ASCII制御文字を使う
# OK: + 短い識別子 + — 日本語にも英数字にも含まれない
TOKENS = {
'UIUXトップディレクター': 'A',
'テックリードエンジニア': 'B',
'トップマーケッター': 'C',
}
def apply(text, old_rules):
for final, tok in TOKENS.items():
text = text.replace(final, tok) # Step1: 新名称を保護
for old, tok in old_rules:
text = text.replace(old, tok) # Step2: 旧名称を置換
for final, tok in TOKENS.items():
text = text.replace(tok, final) # Step3: 復元
return text
第二の落とし穴:バックアップから戻したら今日の作業が消えた
バックアップは毎日03:00取得。22時に事故が発生し、バックアップから復元すると今日中に作成した記事・議事録・公開設定が全て消えた。
偶然残っていた .broken ファイルから救出したが、INSERT の UNIQUE constraint エラーでトランザクションが全ロールバックされ、3回目でようやく完全復元できた。
教訓:バックアップから戻す前に「いつのバックアップか」を必ず確認すること。
再発防止:deploy.sh にホットバックアップを追加
DB_BACKUP_FILE="${BACKUP_DIR}/rhyme_cache-$(date +%Y%m%d%H%M).db"
sqlite3 "$DB_SRC" ".backup '${DB_BACKUP_FILE}'"
ls -t "${BACKUP_DIR}"/rhyme_cache-*.db | tail -n +8 | xargs rm -f # 7世代保持
sqlite3 .backup はコンテナ稼働中・書き込み中でも一貫性のあるスナップショットを取れる。
まとめ
- 保護トークンは「検索パターンの部分文字列を含まない文字列」を使う(
X形式が確実) - バックアップ世代と取得時刻を必ず把握しておく
-
INSERTの UNIQUE 制約違反はトランザクション全体をロールバックする。INSERT OR REPLACEかcommitの分割を使う
#Python #SQLite #個人開発 #インシデント #バグ修正