9 ポイント 投稿者 GN⁺ 2025-04-21 | 1件のコメント | WhatsAppで共有
  • SQLiteの仮想テーブルも書き込みとトランザクションをサポート可能で、xUpdatexSyncxCommitxRollback などのフックを実装して利用する
  • SQLiteは基本的に ロールバックジャーナル方式で原子性 を保証し、複数のDBファイルを扱う場合は スーパージャーナル で全体のコミットを調整する
  • 仮想テーブルもSQLiteのトランザクションプロトコルに含まれxSync が失敗するとトランザクション全体をロールバックする
  • コミットは 2段階に分かれており、xSync は失敗の可能性がある処理xCommit は単純なクリーンアップ処理のみを行うべき
  • xCommitxRollback は常に呼び出されうるため、失敗せず実行できるクリーンアップ用関数として実装すべき

SQLiteの仮想テーブルとトランザクション処理

前回の記事では、Go言語を使ってSQLiteの仮想テーブルを登録し、クエリする基本的な方法を紹介した。今回は、書き込み可能でトランザクションをサポートする仮想テーブルの実装方法を扱う。

仮想テーブルの書き込みとトランザクション対応

  • SQLiteの仮想テーブルインターフェースは 読み取り専用ではない

  • xUpdate フックを実装すれば、外部データソースにも書き込み可能

  • 真のトランザクション整合性のためには、次のようなトランザクションフックが必要:

    • xBegin: トランザクション開始の通知
    • xSync: ディスクへ安全にコミットするための準備(ここで失敗すると全体をロールバック)
    • xCommit: 最終コミットとクリーンアップ
    • xRollback: トランザクションが中断された場合のロールバック処理
  • 通常のテーブルや他の仮想テーブルと一緒に変更される場合でも、SQLiteは すべてのフックを連携させて原子性を保証する

SQLiteトランザクションの内部動作

ロールバックジャーナル (Rollback Journals)

  • SQLiteは基本的に ページを上書きする前にバックアップファイル(ジャーナル)へ保存する
  • 問題が発生した場合はジャーナルから復旧して 原子性を保証する

> 注: SQLiteはWALモードもサポートしているが、この記事の範囲では扱わない

スーパージャーナル (Super-Journals)

  • 複数のデータベースが接続されている場合、各DBの個別ジャーナルだけでは同期が難しい

  • スーパージャーナルという上位レベルのファイルによって、複数ファイル間のコミットを調整する

  • 1つのDBファイル内の複数の仮想テーブルだけを扱う場合は、スーパージャーナルなしでも同期可能

  • いずれの場合でもSQLiteは、トランザクションの流れの中で xSyncxCommitxRollback フックを自動的に呼び出す

仮想テーブルを含む2段階コミット

SQLiteのコミット処理は2段階で行われる:

第1段階: xSync (Durabilityの保証)

  • すべてのB-TreeおよびDBファイルのページまたはジャーナルを ディスクへ安全に同期する
  • 仮想テーブルでもそれぞれ xSync フックが呼び出される
  • どれか1つの xSync で失敗すると、トランザクション全体がロールバックされる → 原子性を維持

第2段階: クリーンアップ (xCommit)

  • ディスクへの保存が完了すると、ジャーナルファイルを削除し、仮想テーブルのクリーンアップを実行する

  • 以下は vdbeaux.c のコードの一部

    disable_simulated_io_errors();  
    sqlite3BeginBenignMalloc();  
    for(i=0; i<db->nDb; i++){  
      Btree *pBt = db->aDb[i].pBt;  
      if( pBt ){  
        sqlite3BtreeCommitPhaseTwo(pBt, 1);  
      }  
    }  
    sqlite3EndBenignMalloc();  
    enable_simulated_io_errors();  
    sqlite3VtabCommit(db);  
    
  • sqlite3VtabCommit() の内部では、実際には すべての xCommit 呼び出しが失敗しても無視される → 純粋なクリーンアップ段階

    int sqlite3VtabCommit(sqlite3 *db){  
      callFinaliser(db, offsetof(sqlite3_module,xCommit));  
      return SQLITE_OK;  
    }  
    
  • xSync によって耐久性が確保されているため、xCommitxRollback は失敗しても無視される

仮想テーブル作成者への注意点

  • 永続性を伴う処理は必ず xSync に入れるべき
    • ネットワークI/O、ファイル書き込みなど失敗しうる処理はここで扱うことで、トランザクションを安全に中断できる
  • xSync の後でも xRollback が呼び出される可能性がある
    • 他のテーブルの xSync が失敗すると、全体がロールバックされる
  • xCommitxRollback は失敗しないクリーンアップ用関数として実装する
    • **idempotent(冪等)**にし、複数回呼び出されても状態変化がないようにすべき

結論

  • SQLiteのジャーナリング機構は、通常テーブルと仮想テーブルを含むすべての要素の原子的コミットを保証する
  • 仮想テーブルのトランザクションフックは、SQLiteのトランザクションフローに 自然に統合される
  • 仮想テーブルを実装する開発者は、xSync に注力してデータ完全性を確保し、クリーンアップ処理は xCommitxRollback に分ける必要がある

1件のコメント

 
GN⁺ 2025-04-21
Hacker Newsのコメント
  • vtabについての記事を見るのはよいことだ。私はRustでSQLiteを再実装しながらvtabサポートを実装した。だから最近vtabについて多くのことを学んだ。vtabは非常に強力で、おそらく十分に活用されていない
  • 興味深い。だが、これはmattnのgo-sqlite3パッケージを使っている。これはCGOだ
    • 現代のGoにおいて、これが一般的または想定される要件なのか気になる