[{"data":1,"prerenderedAt":350},["ShallowReactive",2],{"content-/exam-past-questions-turso-migration":3,"all-pages-for-dir":348,"og-image-/exam-past-questions-turso-migration":349},{"id":4,"title":5,"body":6,"category":330,"description":331,"extension":332,"meta":333,"navigation":334,"ogImage":335,"path":336,"project_name":337,"published":338,"publishedAt":339,"seo":340,"stem":341,"tags":342,"todo":335,"unpublished":338,"updatedAt":335,"__hash__":347},"pages/2026-05/2026-05-27/exam-past-questions-turso-migration.md","ある国家資格試験の過去問データを Turso DB に丸ごと移して全文検索できるようにした",{"type":7,"value":8,"toc":320},"minimark",[9,18,22,25,28,31,41,44,47,69,72,80,83,87,94,100,107,110,181,184,188,195,198,204,224,227,230,244,251,258,262,265,268,271,278,281,316],[10,11,12,13,17],"p",{},"別アプリに散らばっていた、ある国家資格試験の過去問データを Turso DB に丸ごと移した。年度フォルダの奥に ",[14,15,16],"code",{},"exam_XX_batch_NN_NN.json"," という名前で50ファイルほど眠っていたものを、1つのクラウドDBに集約する。やってみたら、JSONを読んで流し込むだけの単純作業だと思っていた手前で何度も足を止めた。トップレベルの構造がファイルごとに揺れていたり、進捗ログが画面に出てこなくてスクリプトが生きているのか分からなくなったり、そもそも会話の履歴が膨らみすぎてツール呼び出しが途中で壊れたり。事実に忠実に、つまずいた順に書き残しておく。",[19,20,21],"h2",{"id":21},"やりたかったこと",[10,23,24],{},"別アプリ（ある国家資格試験の学習アプリ）が持っている過去問データを、独立した Turso DB（libSQL/SQLite互換のクラウドDB）に全部コピーして、データベースとして管理したかった。将来的には Cloudflare 上のDBとして使っていきたい、という展望もある。学習アプリのフォルダにJSONが散らばったままでは、横断的に問題を引くこともできないし、解説や法令参照を後から検索することもできない。まずは「全部1か所に集める」のが今日のゴールだった。",[19,26,27],{"id":27},"まずデータの所在を特定させた",[10,29,30],{},"最初にやったのは、学習アプリのコードベースを調べさせて、過去問データが実際にどこにあるかを突き止めることだった。フォルダを開けると、年度ごとのディレクトリの中に現役データとバックアップが混在していた。",[10,32,33,34,36,37,40],{},"現役データは各年度フォルダの直下にある ",[14,35,16],{},"。",[14,38,39],{},"archive/"," 配下にも似た名前のファイルが並んでいたが、こちらは古いバックアップで、移行対象から外す必要があった。間違ってバックアップを混ぜると、同じ問題が二重に入る。最初にこの線引きをはっきりさせたのが効いた。",[19,42,43],{"id":43},"データ構造を把握する",[10,45,46],{},"バッチファイルを開かせて中身を読むと、3階層になっていた。",[48,49,50,57,63],"ul",{},[51,52,53,56],"li",{},[14,54,55],{},"exam_info"," — 試験回などのメタ情報",[51,58,59,62],{},[14,60,61],{},"question_groups"," — 問題のグループ（問題番号やカテゴリといったメタ）",[51,64,65,68],{},[14,66,67],{},"questions"," — 各設問の選択肢、解説、法令参照",[10,70,71],{},"これとは別に、試験回・分野・カテゴリの定義を持つマスターデータもあった。問題本体とマスターを分けて入れれば、後から「この分野の問題だけ」といった引き方ができる。",[10,73,74,75,79],{},"スキーマの方針はシンプルに決めた。",[76,77,78],"strong",{},"スカラー値（試験回番号、問題番号、選択肢のテキストなど平たい値）はそのまま列にする。解説や法令参照のようにネストした構造は、無理に正規化せず JSON 列にまるごと入れて完全に保持する。"," 正規化を頑張りすぎると、移行スクリプトが複雑になって今日のうちに終わらない。「まず全データを失わずに入れる」を最優先にした。",[10,81,82],{},"Turso スキルを読み込ませて、過去問用の独立DBを新規に作らせた。蔵書用など既存のDBとは混ぜず、過去問だけの専用DBにする。",[19,84,86],{"id":85},"つまずきトップレベルが配列のファイルが混じっていてexit-1","つまずき①：トップレベルが配列のファイルが混じっていてexit 1",[10,88,89,90,93],{},"スキーマを切って、JSONを読んで流し込む移行スクリプトを書かせ、初回実行した。すぐに ",[14,91,92],{},"exit 1"," で落ちた。",[10,95,96,97,99],{},"ログを読むと、あるファイルで「dict を期待したのに list が来た」という趣旨で止まっていた。同じ ",[14,98,16],{}," という名前なのに、トップレベルが dict のファイルと、配列のファイルが混ざっている。",[10,101,102,103,106],{},"そこで全ファイルのトップレベル構造を診断させた。結果、50ファイル中36ファイルがトップレベル dict、残り14ファイルが「dict を1つだけ包んだ配列」だった。つまり一部のファイルだけ、中身は同じなのに ",[14,104,105],{},"[ { ... } ]"," と余計な配列で包まれていた。書き出した時期によって形式が揺れていたのだろう。",[10,108,109],{},"対策は単純で、トップレベルが配列なら最初の要素を取り出し、dict ならそのまま使う、という分岐を1つ入れさせた。",[111,112,117],"pre",{"className":113,"code":114,"language":115,"meta":116,"style":116},"language-python shiki shiki-themes vitesse-light vitesse-light","# トップレベルが配列でも dict でも、中身の dict を取り出す\ndata = raw[0] if isinstance(raw, list) else raw\n","python","",[14,118,119,128],{"__ignoreMap":116},[120,121,124],"span",{"class":122,"line":123},"line",1,[120,125,127],{"class":126},"sxvE3","# トップレベルが配列でも dict でも、中身の dict を取り出す\n",[120,129,131,135,139,142,145,149,152,156,160,163,166,169,172,175,178],{"class":122,"line":130},2,[120,132,134],{"class":133},"sG7-3","data ",[120,136,138],{"class":137},"shFtX","=",[120,140,141],{"class":133}," raw",[120,143,144],{"class":137},"[",[120,146,148],{"class":147},"sM54T","0",[120,150,151],{"class":137},"]",[120,153,155],{"class":154},"sHkkW"," if",[120,157,159],{"class":158},"sz8Xr"," isinstance",[120,161,162],{"class":137},"(",[120,164,165],{"class":133},"raw",[120,167,168],{"class":137},",",[120,170,171],{"class":158}," list",[120,173,174],{"class":137},")",[120,176,177],{"class":154}," else",[120,179,180],{"class":133}," raw\n",[10,182,183],{},"これで14ファイルも問題なく処理できるようになった。外から渡されたデータは、見た目が同じでも構造が揃っているとは限らない。最初に全件の形を診断しておけば、ここはもっと早く抜けられた。",[19,185,187],{"id":186},"つまずき進捗ログが出てこなくて生きているか不安になった","つまずき②：進捗ログが出てこなくて生きているか不安になった",[10,189,190,191,194],{},"構造の分岐を直して再実行したら、今度はスクリプトが何も言わなくなった。標準出力に進捗を ",[14,192,193],{},"print"," していたはずなのに、画面が沈黙したまま動かない。固まったのか、処理を続けているのか、判断がつかない。",[10,196,197],{},"これは Python の出力バッファリングだった。端末（TTY）に直接つながっていないと、標準出力がブロックバッファリングになり、ある程度たまるまで画面に出てこない。プロセスは生きているのに、ログだけが詰まって見えなかった。",[10,199,200,203],{},[14,201,202],{},"-u","（アンバッファ）を付けて実行し直したら、1ファイル処理するごとに進捗がそのまま流れてきた。何件目を処理中か見えるようになって、ようやく安心して待てた。",[111,205,209],{"className":206,"code":207,"language":208,"meta":116,"style":116},"language-bash shiki shiki-themes vitesse-light vitesse-light","python -u migrate_to_turso.py\n","bash",[14,210,211],{"__ignoreMap":116},[120,212,213,216,220],{"class":122,"line":123},[120,214,115],{"class":215},"senZ8",[120,217,219],{"class":218},"snbK4"," -u",[120,221,223],{"class":222},"sdGka"," migrate_to_turso.py\n",[19,225,226],{"id":226},"結果",[10,228,229],{},"最終的に、過去問データを全件 Turso に格納できた。",[48,231,232,235,238,241],{},[51,233,234],{},"試験10回分",[51,236,237],{},"問題 499 件",[51,239,240],{},"選択肢 1992 件",[51,242,243],{},"解説・法令参照などのネスト構造は JSON 列で完全保持",[10,245,246,247,250],{},"ローカルのレプリカファイルを介さず、HTTP接続でクラウドのDBまで到達できることも確認した。実際にクラウド側へ届いているかを確かめないと、ローカルにだけ書いて満足するという事故が起きうる。あわせて、移行で増えた新しいレプリカファイルが ",[14,248,249],{},".gitignore"," の除外対象に入っているかも確認した。DBの実体をうっかりリポジトリにコミットしたくない。",[10,252,253,254,257],{},"最後に、今日やった作業と今後の計画を ",[14,255,256],{},"memo/2026-05-27/"," 配下にチェックボックス形式でドキュメント化した。Cloudflare 上での運用に向けて、次に何をやるかを残しておく。",[19,259,261],{"id":260},"この日いちばんのハマり会話履歴の肥大でパースエラーが頻発した","この日いちばんのハマり：会話履歴の肥大でパースエラーが頻発した",[10,263,264],{},"実は移行作業そのものより、この日いちばん時間を持っていかれたのは別のところだった。",[10,266,267],{},"冒頭でコードベースを調査させたとき、venv のパスや大量のファイルパスが会話の履歴にどんどん流れ込んでいた。その状態で次の作業を頼むと、ツール呼び出しを生成している途中で出力が崩れ、「The model's tool call could not be parsed」というエラーが何度も出た。一度出ると、同じ調子で連発する。",[10,269,270],{},"最初はツール側の不調を疑ったが、よく見ると共通点があった。履歴が膨らんでいるときほど壊れる。真因は会話履歴の肥大だった。一度に大きな調査をさせて出力をため込むと、その後のツール呼び出しが不安定になる。",[10,272,273,274,277],{},"対策は地味だが効いた。",[76,275,276],{},"1回ずつ小さく刻んで進める。"," 大きな調査を一度に投げず、ファイルを絞って読ませ、結果を受け取ってから次へ。出力を膨らませない進め方に切り替えたら、パースエラーはぴたりと止まった。",[19,279,280],{"id":280},"学びメモ",[48,282,283,289,295,304,310],{},[51,284,285,288],{},[76,286,287],{},"会話履歴を肥大させない。小刻みに刻んで進める。"," venv や大量パスを一度に流し込むと、その後のツール呼び出しが「could not be parsed」で壊れ始める。大きな調査ほどファイルを絞って分割する",[51,290,291,294],{},[76,292,293],{},"外部由来データは、トップレベル構造のばらつきを最初に診断する。"," 同じ命名規則のファイルでも dict と「dict を包んだ配列」が混在していた。流し込む前に全件の形を見ておけば、exit 1 で止まらずに済んだ",[51,296,297,300,301,303],{},[76,298,299],{},"非TTYでは標準出力がブロックバッファリングされる。"," 進捗が見えないときはプロセスが死んだとは限らない。Python なら ",[14,302,202],{}," を付けるだけでログがそのまま流れる",[51,305,306,309],{},[76,307,308],{},"正規化を頑張りすぎない。"," スカラーは列、ネストはJSON列、という割り切りで「まず全データを失わずに入れる」を先に達成した。設計の磨き込みはDBに入ってからでも遅くない",[51,311,312,315],{},[76,313,314],{},"クラウドへ実際に届いたかを確認する。"," ローカルのレプリカに書けただけで安心せず、HTTP接続でクラウド側まで到達したことを確かめる",[317,318,319],"style",{},"html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}",{"title":116,"searchDepth":130,"depth":130,"links":321},[322,323,324,325,326,327,328,329],{"id":21,"depth":130,"text":21},{"id":27,"depth":130,"text":27},{"id":43,"depth":130,"text":43},{"id":85,"depth":130,"text":86},{"id":186,"depth":130,"text":187},{"id":226,"depth":130,"text":226},{"id":260,"depth":130,"text":261},{"id":280,"depth":130,"text":280},"dev","別アプリに散らばっていた過去問JSONを Turso へ一括移行した記録。スカラー値は列、解説や法令参照のネスト構造はJSON列で保持した。配列ラップ構造の混在やstdoutバッファリング、会話履歴肥大によるパースエラーまで、つまずいた過程を残す。","md",{},true,null,"/exam-past-questions-turso-migration","misc-dev",false,"2026-05-27T00:00:00.000Z",{"title":5,"description":331},"2026-05/2026-05-27/exam-past-questions-turso-migration",[343,344,345,346],"Turso","SQLite","Python","データ移行","9pl_-mh9FiB1ZrG6s9JIRgcpyd8y5erI_p42vygSsW0",[],"https://log.eurekapu.com/og/blog/exam-past-questions-turso-migration.png?v=2026-05-27T00%3A00%3A00.000Z&title=%E3%81%82%E3%82%8B%E5%9B%BD%E5%AE%B6%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93%E3%81%AE%E9%81%8E%E5%8E%BB%E5%95%8F%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%20Turso%20DB%20%E3%81%AB%E4%B8%B8%E3%81%94%E3%81%A8%E7%A7%BB%E3%81%97%E3%81%A6%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%9F&author=Kei%20Komatsu&sig=c388ce50d284fd3f",1782528842997]