Skip to content

Latest commit

 

History

History
767 lines (613 loc) · 70.2 KB

File metadata and controls

767 lines (613 loc) · 70.2 KB

🗃️ ExecDB 仕様書 (Specification Draft)

English version: execdb_spec.md

ExecDB は、DBエンジンとデータ領域を1つの実行ファイル内に保持する、環境構築不要のポータブルな単一バイナリRDBMSです。

License: MIT


1. コアコンセプト

  • Data-in-Binary (単一バイナリ完結): 実行ファイル内にデータ領域を持ち、外部のDBファイルや複雑な環境構築、Docker Volumeマウントを一切不要にします。
  • In-Memory Operations: 起動時に全データをメモリ上に展開し、すべてのクエリ操作をメモリ速度(超低遅延)で処理します。
  • Snapshot Persistence (別名実行ファイルの出力): 停止時(または実行中の保存指示)に、メモリ上の最新データを保持した**新しい実行ファイル(別名)**を生成して永続化します。元の実行ファイルを維持することで、安全なスナップショット作成やデータ配布を容易にします。
  • Zero-Auth & Lightweight: ユーザー管理、権限、ログインなどの概念を全削ぎし、ローカル開発・テスト・コンテナ環境でのシームレスな使い勝手を追求します。デフォルトはZero-Authですが、--user(§9)でオプトインの手軽な認証(単一の名前+パスワードのキー、ユーザー管理の概念は持たない)を有効化することもできます。

CLI出力言語: REPLのメッセージ、エラーメッセージ、--help、ログ出力等、CLIが出力する文字列は基本的に英語とする。国際的なOSSとしての公開・利用を想定するため。

ログ出力先: ログ・診断メッセージは標準エラー出力(stderr)のみとし、ログファイルの生成・ローテーション等の機構は持たない。単一バイナリで完結するというコアコンセプトに合わせ、副産物としてのログファイルを生成しない設計とする。REPLのクエリ結果は標準出力(stdout)に出力するため、両者は分離される。--no-replでサーバーモード稼働させる場合の永続的なログ保存は、2> execdb.logのようなリダイレクトや、Docker/systemd等の既存運用基盤に委ねる。-q/--quiet(§9)で出力自体を抑制することは可能。


2. インターフェースとアクセス制御

安全な運用とシンプルなアクセス制御のため、操作経路によって実行できるクエリの種類(アクセス権限)を明確に分離します。

インターフェース 役割 許可される操作
対話式コンソール (REPL) 起動時にターミナル上で直接開始。構造定義およびデータ管理用。 DDL, DML, TCL, 専用制御コマンド
外部 I/F (PostgreSQL互換ワイヤープロトコル) バックグラウンドで待機。アプリケーションや外部ツールからのデータ操作用。 DML, TCL (※ DDLは拒否)

クエリ種別の定義

  • DDL (データ定義言語): CREATE TABLE, DROP TABLE, ALTER TABLE, CREATE VIEW, CREATE INDEX, CREATE TRIGGER
  • DML (データ操作言語): SELECT, INSERT, UPDATE, DELETE
  • TCL (トランザクション制御言語): BEGIN, COMMIT, ROLLBACK

同時実行制御 (Concurrency Control)

REPL と外部 I/F は、内部SQLエンジンに対する2つの独立したクライアントとして扱う。ロック・トランザクション分離レベルなどの同時実行制御は、ExecDB独自には実装せず、内部SQL互換エンジン(7章参照)が標準で備える機構にそのまま準拠する。ExecDB側のアクセス制御レイヤー(本章の許可/拒否ルール)と、エンジン側の排他制御レイヤーは役割を分離する。

実現手段: REPL・外部I/Fの1接続はそれぞれ、engineパッケージのDB.Session(ctx)が返す専有SQLiteコネクション(内部的にはmodernc.org/sqlitememdb VFS上で同一インメモリDBを指す別コネクション、7章参照)に対応する。BEGIN/COMMIT/ROLLBACKはそのコネクション上でSQL文としてそのまま実行され、他コネクションとのロック衝突時の待機・失敗はSQLite自身のbusy-handler機構(busy_timeout)に委ねる——ExecDBが独自にロックやキュー、コネクション間の調停を実装することはない。なおengine.DBがGoレベルで持つ排他制御(sync.RWMutex)は、コネクションの差し替えやCloseなどDB自体のライフサイクル・メタデータ(Info()が返す情報等)を保護するためのものであり、SQL実行そのものの同時実行制御ではない(両者は明確に別レイヤーであり、上記の原則と矛盾しない)。

SET/SHOW等のセッションコマンド(第3の区分)

外部I/F経由で受け取る文は、上表の「DML/TCLは許可・DDLは拒否」という二分法だけ では説明できないものが存在する。PostgreSQLドライバ(特にpgJDBC)は接続直後に SET extra_float_digits = 3のようなセッションパラメータ設定コマンドを自動的に 送ってくるが、SQLiteにはSET/SHOWという文自体が存在しないため、そのまま 内部SQLエンジンへ渡すと構文エラーになり、接続そのものが確立できない。

SET/SHOWは「許可してSQLiteへ渡す」でも「DDLのように拒否する」でもない 第3の区分として扱う。外部I/F層が内部SQLエンジンへ渡す前に横取りし、 接続ごとのメモリ上のマップへ値を保持するだけで(SQLite側の設定は実際には 何も変更しない)、SETにはCommandComplete("SET")を、SHOWにはその値を 1列1行の結果として返す。DDLの拒否のようなErrorResponseは返さない—— ドライバから見ればコマンドが正常に成功したように見える必要があるため。 詳細は§8参照。


3. REPL コマンド体系

対話式コンソール(REPL)では、SQL文に加えて . で始まる専用制御コマンド(ドットコマンド)を実行できる。コマンド体系は SQLite CLI (sqlite3) を踏襲しつつ、ExecDBの単一インメモリDBというコンセプトに合わせて取捨選択する。

採用する基本コマンド(SQLite踏襲)

コマンド 役割
.tables テーブル一覧表示
.schema [table] CREATE文(スキーマ)表示
.exit [CODE] / .quit [CODE] REPL終了(自動保存は行わない。保存確認のプロンプトも出さず即座に終了する。§4参照。CODE省略時は正常終了、指定時はそのコードで即座にプロセスを終了する)
.help コマンド一覧表示
.headers on|off 結果表示にカラム名を出すか
.mode MODE 出力形式。list(既定、|区切り・ヘッダなし)/column(列幅揃え、切替時.headers自動on)/csv(RFC 4180、CRLF)/json(配列。BLOBは16進文字列)/line(1列1行)の5種のみ採用する(quote/insert/tabs/markdown/box/html等の装飾系はCLI出力方針(.claude/rules/cli-output.md)により不採用)
.import FILE TABLE CSVファイルをテーブルへ読み込む。.modeの設定に関わらず常にCSVとして読む(sqlite3は.modeに連動するが、「.importは常にCSV」と単純化——意図的な相違)。TABLEが存在しなければ1行目を列名として全TEXT列で自動CREATE、存在すれば1行目からデータとして扱う。行ごとのフィールド数が列数と一致しない場合はその行番号を含むエラーで処理全体を中断し、1行も投入しない(sqlite3は警告のうえ補完/切り捨てして継続するが、シードデータ投入という主用途では黙って歪んだデータが入る方が有害と判断——意図的な相違)
.dump [PATTERN] PATTERN(SQLのLIKEパターン、省略時は全テーブル)に一致するテーブルのスキーマ・データと、それらに属するindex/view/triggerをSQL文としてダンプする。値のリテラル化はSQLite自身のquote()関数に委譲する

.import / .dump は、外部データ連携用途に加えて、開発・テスト時にシードデータの投入や状態のスナップショット確認に使えるため採用する。

ExecDB独自で追加するコマンド

コマンド 役割
.snapshot [FILENAME] [--timestamp] スナップショット保存(§4, §9参照)。--timestamp指定時はファイル名にタイムスタンプを付与
.overwrite 自身の実行ファイルを最新データで上書きし、成功したらREPLを終了する(§4, §7参照)
.load <取り込むファイル名> 別のExecDBファイルからデータのみを取り込み、メモリ上のDB状態を置き換える(ファイルは生成しない。§4参照)

採用しないコマンド

ExecDBは常に単一のインメモリDBのみを扱うシンプルな構成のため、以下のSQLite由来のコマンドは採用しない。

  • .open / .databases(複数DB切り替えの概念が無いため)
  • .backup / .restore(ExecDB独自の .snapshot 方式と役割が重複するため)
  • .session(変更履歴セッション機能。過剰な機能のため対象外)

4. 保存・永続化仕様 (Snapshot Mechanism)

データ保持は**「新しい実行ファイル(別名)の生成」**によって行われます。

  • 生成タイミング:
    • .snapshot コマンドによる明示保存のみ: 対話コンソール等から手動指示があった場合にのみスナップショットを生成する。
    • プロセス停止時の自動保存は行わない: SIGTERMや、Ctrl+C(SIGINT)が実際にプロセスを終了させた場合(対話モードでのCtrl+Cの詳しい挙動は§10参照。常に即終了するわけではない)、メモリ上のデータは保存されず消える。ExecDBはメモリDBとしての揮発性を前提とし、永続化はユーザーの明示的な操作(.snapshot)に一本化することで、停止処理中の書き込み中断による破損リスクや、不要なスナップショットの自動増殖を避ける。
  • ファイル命名規則:
    • デフォルトでは日時タイムスタンプを付与した実行ファイルを自動生成(例: execdb_20260831_143000)。
    • コマンドライン引数や対話コンソールからの指定により、任意のファイル名での出力や既存ファイルへの上書き更新にも対応。
  • スナップショットファイルの管理: .snapshot のたびにエンジン本体+データのフルコピーが新規生成されるため、実行のたびにファイル数・容量が増加していく。古いスナップショットの整理(削除)は仕様の対象外とし、運用側(ユーザー)の判断に委ねる。

自己上書き(.overwrite)という例外

.overwrite は、.snapshot と同じくユーザーの明示操作による保存だが、保存と同時にプロセスを終了する点のみが異なる。上記「プロセス停止時の自動保存は行わない」という原則から唯一逸脱する例外操作であり、学習用途・軽量CLIツールとして「編集してそのまま閉じる」という直感的なワークフローを提供するために意図的に許容する。書き込み方式(実行中ファイルへの上書き回避手順)は§7を参照。

他ファイルからのデータ取り込み(.load

.load <取り込むファイル名> は、別のExecDBファイルに埋め込まれたデータのみを取り込み、今動いているプロセスのメモリ上のDB状態をそのデータで置き換えるコマンドである。ファイルの生成は行わない(メモリ上の状態を変更するだけ)。主なユースケースは、異なるOS/アーキテクチャ向けにビルドされたExecDBファイル間でのデータ移植(例: Linux上で作成したデータを、Windows用の空バイナリへ取り込んでから配布する)。

  • 動作: <取り込むファイル名> からデータブロブ(§7参照)のみを読み出し、SQLiteエンジンへ取り込むことで(内部実装は§7参照)、今動いているプロセスのメモリ上のDB状態を置き換える。今動いているプロセスが元々持っていたデータは完全に置き換わる(マージはしない)。
  • ファイルへの反映: .load 自体はファイルを生成しない。取り込んだ状態をファイルとして保存したい場合は、続けて .snapshot (別名保存)または .overwrite (自己上書き)を実行する。異なるOS向けバイナリへデータを移植する典型的な流れは次の通り: 対象OS向けの空バイナリを起動 → .load <取り込むファイル名> でデータをメモリへ取り込む → .overwrite でそのバイナリ自身にデータを書き込む。
  • エンジン部分は変更されない: .load は取り込み元ファイルのエンジン部分を一切参照しない。常に今動いているプロセス自身のエンジンで、データのみを読み込む。異なるOS向けバイナリのエンジン部分を直接取り込むことはできない。
  • 既存セッションへの影響: .loadはメモリ上のDBを、生きているデータベースへの上書き(§7参照)という形で置き換えるため、その時点で既に開いている他のセッション(REPL自身や外部I/Fの別接続、engine.Session)は接続自体が失われることなく維持され、以降に発行する文からは置き換え後のデータをそのまま参照できる。ただし.load実行中に他のセッションが未コミットの書き込みトランザクションを実行中だった場合は、SQLite自身の排他制御(本章「同時実行制御」)に従い、.load側が待機または失敗しうる。
  • データを持たないファイルへの.load: 取り込み元ファイルにExecDBのデータブロブが存在しない(フッターのMagicが無い、またはデータ長が0の)場合はエラーとなり、今動いているプロセスのメモリ上の状態は変更されない。
  • バージョン不一致の扱い: 取り込み元ファイルのフッター(Versionフィールド、§7参照。データブロブのフォーマットバージョンであり、ExecDBというソフトウェア自体のバージョン——例えばGitタグ——とは別の値)が、今動いているプロセスが書き出すフッターのVersionと異なる場合、警告を表示した上で処理を続行する(拒否はしない)。この警告表示はcmd/execdb側(呼び出し側)の責務であり、engineパッケージ自体はログ出力を行わない(§6の責務分担、ライブラリはstderrに書かない)。engineInspect(path)でフッター情報のみを取得するAPIを提供するので、cmd/execdb.load実行前にこれを呼び、バージョン不一致を検出したら自分でメッセージを出す。
  • §4の原則との関係: .load.snapshot/.overwrite と同様、ユーザーの明示操作によってのみ実行される。自動的に他ファイルのデータを読み込む機構は存在しない。.load 自体はメモリ状態を変更するのみで、ファイルへの永続化は伴わない点に注意する(保存には別途 .snapshot/.overwrite が必要)。

5. 主なユースケース (Use Cases)

  1. CI/CD・E2Eテストの爆速化 (Instant Test DB)
    • テーブル構築・シードデータ投入済みのバイナリを1個置くだけで、1秒未満でメモリ上へテスト環境が立ち上がり、後処理も不要。
  2. 「データ状態付き」でのバグ再現・チーム共有 (Executable Snapshots)
    • 不具合が起きたデータ状態を .snapshot bug_123 でバイナリ化して共有。受け取った側は実行するだけで全く同じDB環境を即座に再現。
    • 共有先が異なるOSを使っている場合は、そのOS向けの空バイナリを起動し、.load でデータをメモリへ取り込んだ上で .overwrite を実行することで、同じデータをそのOS向けの実行ファイルとして作り直せる(§4参照)。
  3. 脱・環境構築のモックAPI / デモ環境 (Zero-Config Mock Server)
    • 対話コンソールでデータを作り、そのまま外部I/F(PostgreSQL互換プロトコル)として公開。既存のDBクライアントツールやORMからそのまま接続でき、フロントエンド開発やクライアントデモに最適。
  4. エッジ・CLIツールのポータブルな状態管理 (Portable Data Capsules)
    • 外部DBに依存せず、実行ファイル単体で高速処理とファイル単位のスナップショット保存を実現。
  5. 環境構築ゼロのSQL学習・ハンズオン (Zero-Setup SQL Sandbox)
    • インストールや認証設定なしで、バイナリを叩くだけで即座にフル機能のSQL(View/Index/Trigger/Transaction)を学習・実験。
    • Windows / macOS / Linux 問わず、実行ファイルをダブルクリック(またはターミナルから起動)するだけでREPLが立ち上がるため、WSL2等の追加セットアップを必要とせず初学者でも即座に試せる。

6. ライブラリアーキテクチャ

ExecDBは、**インメモリSQLエンジンのライブラリ(engineパッケージ)**を核とし、現行のスタンドアロン実行ファイル(REPL+外部I/F+バイナリ内蔵の単一バイナリRDBMS)は、そのengineを用いて構築された「1つのリファレンス実装」として位置づける。

レイヤー構成

execdb/
├── engine/              ← 【ライブラリ本体】インメモリSQLエンジン
│                            (modernc.org/sqlite ラッパー、Go関数呼び出しのAPI、
│                             永続化インターフェース、自身の実行ファイルを
│                             上書きする Overwrite を提供)
│
└── cmd/execdb/          ← 【アプリ】engineを利用した単一バイナリRDBMSの実装
                             (REPL、外部I/F(PostgreSQL互換ワイヤープロトコル)、
                             DDL/DML/TCLのアクセス制御(§2)を担う)

具体的なファイル構成(engine.go, persist.go, main.go, repl.go, pgwire.go 等)は実装時に決める(詳細は .claude/rules/directory-structure.md 参照)。ここでは2つのディレクトリの役割分担のみを示す。

責務分担

engine(ライブラリ) cmd/execdb(アプリ)
SQL実行 Go関数呼び出し(単発実行のdb.Query()/db.Exec()等、および独立したクライアント単位でBEGIN/COMMITをまたぐdb.Session(ctx))を提供 engineのAPIを呼び出すだけ
ネットワークI/F 持たないnetnet/httpへの直接依存なし) PostgreSQL互換ワイヤープロトコルを実装
アクセス制御(DDL/DML/TCL) 制限なし(呼び出し元コードは信頼済みとみなす) REPL/外部I/Fの経路ごとに制御(§2)
永続化 汎用的な読み書き(任意のファイル、io.Writer/io.Reader)+ 自身の実行ファイルへの上書き(Overwrite .snapshot / .overwrite / .load コマンドから呼び出すだけ(§4)
用途 他のGoアプリに組み込む内蔵DB層(sqliteファイルの代替) CI/CDテスト、モックAPI、ポータブル配布

設計原則: ネットワークI/Fを持たないことによる安全性

engineパッケージは、Goの関数呼び出しでのみアクセスできる。ネットワーク越しの入出力機能を実装しないため、他のGoアプリへ組み込んだ場合、外部からの通信経路はそのアプリ自身が明示的に作らない限り一切存在しない。これは「弾くロジックを書いて防御する」のではなく、「弾く手段が構造的に存在しない」ことによる安全性であり、engine自身がnetnet/httpをimportしないことはコードレベルで検証可能である(この設計は1章の「Zero-Auth」思想と一貫する)。ただし推移的依存にはnetパッケージが含まれる(内部SQLエンジンmodernc.org/sqliteが使うmodernc.org/libc経由)。これはSQLiteドライバ実装自体の都合であり、engine自身がネットワークI/Oを行うコードを持つことを意味しない——検証すべきは「直接importの有無」および「net/httpが推移的にも現れないこと」である。

自己上書き(Overwrite

engineパッケージは、自身を組み込んだホストアプリの実行ファイルを、最新のDB状態で上書きするための関数を提供する。

// engine パッケージ
// Overwrite は、呼び出し元プロセス自身の実行ファイル(os.Executable()で得られるパス)を、
// 現在のDB状態を埋め込んだ内容で上書きする。書き込み方式は§7参照。
// 成功時、ファイルの中身は差し替わるが、呼び出し元プロセス自体は終了しない
// (終了させるかどうかは呼び出し元の判断に委ねる)。
func (db *DB) Overwrite() error

engineは「自分のどこまでがエンジンでどこからがデータか」をホストアプリに意識させない。ホストアプリ全体(engineを組み込んだアプリ本体一式)がそのまま「エンジン部分」として扱われ、その後ろにデータとフッターが付与される。ExecDB本体の .overwrite コマンド(§3)も、内部でこの Overwrite を呼んでいるだけの薄いラッパーである。

利用イメージ(ホストアプリ側):

if err := db.Overwrite(); err != nil {
    log.Fatal(err)
}
os.Exit(0) // 「編集して閉じたら、次回起動時に続きから」という体験を実現

利用上の注意:

  • ホストアプリが /usr/bin/C:\Program Files\ のような書き込み権限が必要なディレクトリに設置されている場合、Overwrite は権限エラーで失敗する。
  • ホストアプリ自身が別の自己更新の仕組み(Squirrel/Sparkle等)を持つ場合、実行ファイル差し替えのタイミングが競合しうるため注意する。
  • このAPIはGoアプリへの組み込みを対象範囲とする。他言語アプリへ同じロジックを移植することは可能だが、本仕様のスコープ外とする。

OpenSelf — 自身の実行ファイルからのロード

cmd/execdb本体のように、「自分自身の実行ファイルに埋め込まれたデータを読み込む」 ホストアプリ向けに、engineは専用の入口を提供する。内部的には os.Executable()で自身のパスを取得した上でOpenを呼ぶだけの薄いラッパーだが、 起動時に前回のOverwriteが残した.execdb_old退避ファイル(§7参照)の ベストエフォート削除も合わせて行う。

// engine パッケージ
// OpenSelf は、実行中のプロセス自身の実行ファイル(os.Executable())に
// 埋め込まれたデータを読み込む。
func OpenSelf() (*DB, error)

Session — 独立したクライアント単位の専有コネクション

db.Query()/db.Exec()はコネクションプールから借りた接続を1文だけ使って 返す単発実行であり、これを使ってBEGINしても、後続のCOMMITが同じ接続に 届く保証はない(2章「同時実行制御」参照)。REPL・外部I/Fのように「1つの 独立したクライアントが、同じ接続上で複数の文にまたがるトランザクションを 張る」用途には、Sessionを使う。

// engine パッケージ
// Session は、db が保持するインメモリDBに対する専有コネクションを返す。
// 独立したクライアント1つ分(外部I/Fの1接続、REPLのプロセス自体、等)に
// つき1つ取得することを想定する。BEGIN/COMMIT/ROLLBACKはこのコネクション上で
// 通常のSQL文として実行すればよく、Go側に専用のトランザクションAPIは無い。
func (db *DB) Session(ctx context.Context) (*Session, error)

func (s *Session) Exec(query string, args ...any) (sql.Result, error)
func (s *Session) Query(query string, args ...any) (*sql.Rows, error)
func (s *Session) QueryRow(query string, args ...any) *sql.Row
// 上記それぞれの *Context 版、および db.Exec 等自体の *Context 版もある。

// 同じ文を繰り返し実行する呼び出し元(cmd/execdbの.importによる
// CSV一括投入等)向けに、このSessionの専有コネクション上でprepareした
// *sql.Stmt を返す。
func (s *Session) Prepare(query string) (*sql.Stmt, error)
func (s *Session) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)

// 使い終えたら明示的に Close する(idempotent)。
func (s *Session) Close() error

ライブラリとしての利用例

import "github.com/amisonnet8/execdb/engine"

// 専用データファイルを開く(sqliteファイルの代替として利用)
db, err := engine.Open("myapp.execdb")

// 通常のSQL操作(アプリ内部コードは信頼済みのためDDL/DML制限なし)
db.Exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
db.Query("SELECT * FROM users")

// 永続化(別ファイルへ明示的にスナップショットを書き出す。ファイル名は
// 呼び出し側が指定する。タイムスタンプ付与等の命名規則はcmd/execdb側の責務)
db.Snapshot("myapp_backup.execdb")

// 永続化(自分自身の実行ファイルを上書きする。ここでは常に
// os.Executable() が対象になり、Open() に渡したパスとは無関係)
db.Overwrite()

db.Close()

7. アーキテクチャ・技術選定

項目 内容 備考
開発言語 Go (Golang) 静的バイナリ生成、クロスコンパイル、マルチプラットフォーム対応。
ターゲットOS クロスプラットフォーム対応(Linux / macOS / Windows) REPL単体起動・外部I/Fは全OS共通で動作保証。UNIX Domain Socketはローカル接続向けの追加オプション(Windowsでの利用可否は環境依存、Windows 10 Build 17063以降/Server 2019以降が目安)。コンテナ運用(FROM scratch等の軽量イメージ)はLinuxを優先する。配布物は各OS/アーキテクチャ向けの空(データなし)バイナリをあらかじめビルドして提供する。
内部DBエンジン modernc.org/sqlite(Pure-Go SQLite トランスパイル版) CGO不要でクロスコンパイル容易、database/sql標準インターフェース準拠。View, Index, Trigger, Transaction を含む完全な SQL 互換性を実績豊富な実装で低コストに担保。
外部 I/F PostgreSQL互換ワイヤープロトコル(サブセット) 詳細は§8参照。JDBC/psycopg/node-postgres等の既存ドライバ資産にそのまま乗る。

バイナリ埋め込み方式(固定長フッター)

実行ファイル末尾へのデータ埋め込みは、固定長フッター(トレーラー)方式を採用する。

[Goバイナリ本体(エンジン部分)] [データブロブ(DB状態)] [フッター(固定32バイト)]
フィールド サイズ 内容
Magic 8 bytes 識別子(例: "EXECDB01"
Version 4 bytes フォーマットバージョン(big-endian符号なし整数)
DataOffset 8 bytes データブロブの開始オフセット(先頭からの絶対位置、big-endian
DataLength 8 bytes データブロブの長さ(big-endian
Reserved 4 bytes 将来拡張用

フッター中のすべての整数フィールドは big-endian でエンコードする。

  • 読み込み: os.Executable() で自身のパスを取得し、ファイル末尾から32バイトを Seek で読むだけでフッターを特定できる。ELF/Mach-Oのセクションヘッダ解析は不要。

  • 書き込み(.snapshot実行時、別名保存): 起動時に自身のエンジン部分のオフセット(0〜DataOffset、または末尾フッターが無い場合はファイル全体サイズ)のみをメモリ上に保持しておき、実際のエンジンバイト列は保存直前に読み直す(modernc.org/sqlite込みのバイナリは10〜15MB級になるため、バイト列自体を常駐させない)。保存時は「エンジンバイト+新データ+新フッター」から新しいスナップショットを生成する。差分計算や再リンクは不要。書き込みは同じディレクトリに作った一時ファイルへ書いてからrenameするアトミックな方式を採り、読み手が書きかけの不完全なファイルを観測することはない。また、データブロブの取得(後述のSerialize)は他セッションが書き込みトランザクションを実行中でないことを保証してから行うため、並行して書き込みが起きていても常に一貫したスナップショットになる。

  • 書き込み(.overwrite実行時、自己上書き): 同名(自分自身)への直接上書きは、実行中のファイルへの書き込みロックによりOSレベルで拒否される(LinuxのETXTBSY、WindowsのERROR_SHARING_VIOLATION)。これを回避するため、以下の手順を採る(Linux/Windows双方でPoC検証済み)。

    1. 実行中の自分自身を <path><path>.execdb_oldrename で退避する(実行中ファイルの名前変更自体はLinux/Windows双方で許可される)
    2. 空いた元のパスへ新しい中身(エンジンバイト+新データ+新フッター)を新規書き込みする
    3. 退避ファイルの削除を試みる(Linuxはその場で成功。Windowsは実行中のためロックされ失敗するのが正常であり、次回起動時にベストエフォートで削除する)

    手順2の書き込みに失敗した場合は、手順1で退避したファイルを元のパスへベストエフォートで戻す(=完全に失敗する前の状態への復帰を試みる)。また、実行中のプロセスがgo runによって生成された一時バイナリである場合(プロセス終了と同時にOSがファイル自体を削除するため、上書きしても無意味になる)、.overwriteは明示的なエラーで拒否する。

  • データなしの初回配布バイナリ: 末尾にマジックバイトが見つからない場合は「エンジンのみ・データ空」として起動する。go build した素のバイナリがそのまま実行可能であり、.snapshot または .overwrite を実行して初めてデータ付きバイナリになる。

  • この方式は自己解凍インストーラやJARファイル(ZIP中央ディレクトリの末尾探索)などで実績のある手法であり、OSローダーの実行にも影響を与えない。

データブロブのシリアライズ方式(確定)

「データブロブ」=インメモリSQLite DBの状態をバイト列化する方法として、SQLite標準の Serialize/Deserialize API(sqlite3_serialize() / sqlite3_deserialize())を使う方式を採用する(2026-09-04、フェーズ①Step 1の実測検証済み。フォールバック不要と判断。詳細な検証記録はPLAN.mdのフェーズ①/②の「確定した事実」各節を参照)。自前のシリアライズ形式は編み出さない。

  • 実際のAPI: modernc.org/sqlite(v1.58.0で確認)が提供するのは func (c *conn) Serialize() ([]byte, error)func (c *conn) Deserialize(buf []byte) error であり、スキーマ名を渡す 引数は無い(常にmainスキーマが対象。当初想定していた Serialize("main") / Deserialize("main", data) という形ではない)。conn 型自体はunexportedだが メソッドはexportedなので、database/sql*sql.Conn から conn.Raw(func(driverConn any) error { ... }) を呼び、driverConn を ローカルで定義したinterface(interface{ Serialize() ([]byte, error) } 等)に 型アサーションすることで到達できる。
  • 書き込み時(.snapshot / .overwrite): 上記の方法で取得した Serialize() の 戻り値を、そのままフッター方式の「データブロブ」として書き込む。
  • 起動時: バイナリ末尾から読み込んだデータブロブを、同様の方法で取得した Deserialize(data) にそのまま渡すことで、SQLiteが内部的にインメモリDBとして 再展開する。modernc.org/sqlite は内部で SQLITE_DESERIALIZE_RESIZEABLE|SQLITE_DESERIALIZE_FREEONCLOSE を指定して sqlite3_deserialize() を呼んでいるため、復元後のDBは通常のDBと同様に 拡張可能(大量INSERTが可能。復元専用の固定サイズDBにはならないことを実測 確認済み)。
  • 接続共有モデル(memdb VFS): DSNはfile:/<name>?vfs=memdb&_busy_timeout=<N>modernc.org/sqliteが実装するmemdb VFS)を採用する。名前が/で始まる DSNは、同一プロセス内であれば複数のコネクションが同じインメモリストアを 共有する。memdbのロックはファイルベースDBと同種のSHARED/RESERVED/ EXCLUSIVE機構で実装されているため、コネクション間のロック衝突時は busy_timeout(SQLite標準のbusy-handler機構)が正しく機能し、有界に 待ってから諦める(2章「同時実行制御」参照)。当初検討していた file:<name>?mode=memory&cache=shared(shared-cache)は、ロック衝突時に SQLITE_LOCKED_SHAREDCACHEという別系統のエラーとなり、busy_timeoutが 効かないうえ、待機自体もアプリケーション側からcontextでキャンセルできず 無期限にハングしうることが実測で判明したため不採用とした。他の全接続が 閉じてもストアが解放されないよう、Close()まで保持し続ける「keeper接続」 を1つ持つ点は変わらない(keeperはストアを生かすためだけに存在し、SQL文の 実行そのものには使わない)。
  • Deserializeと複数コネクションの関係: Deserializeはスキーマを 匿名(無名)のmemdbストアとして内部的に開き直す実装になっており、 呼び出したコネクション自身にしか結果が反映されない——同一DSNで後から 開いた別コネクションからも見えない。そのためOpen/OpenSelf/Loadは、 まず使い捨てのコネクションへDeserializeした上で、SQLite標準の オンラインBackup API(sqlite3_backup_*系。modernc.org/sqliteでは NewBackup/NewRestoreとして公開)でその内容を「生きている」DB (keeper接続が保持するストア)へコピーする、という2段階の手順を踏む。 Backup APIによるコピーはSQLite本来のB-tree/pager経由の複製であり、 Deserializeと異なり、コピー先の全コネクション(Backup実行前から 開いていたものも含む)から結果が見える。Loadがこの仕組みで「生きている」 DBへその場で上書きする形を取ることにより、.load実行前から開いていた 他のセッション(4章参照)も接続を失わずに新しいデータを参照できる。
  • 既知の制約: SQLite本体のSerialize/Deserialize自体は非連続な バイト列を扱えず、理論上はデータベースサイズが2GB未満に制限される (SQLiteが一度に2GBを超えるメモリを確保しないため)。ただし採用した memdb VFSにはそれとは別にSQLITE_MEMDB_DEFAULT_MAXSIZE(1GiB)という デフォルト上限があり、実測では約960MiB付近でSQLITE_FULLとなることを 確認済み——実効的な上限はこちらの約1GiBである。ExecDBの主要ユースケース (CI/CDテストDB、モックAPI、学習用サンドボックス等)では通常問題になら ない想定だが、大容量データを扱う用途では制約となりうる。

8. 外部 I/F プロトコル仕様(PostgreSQL互換ワイヤープロトコル)

外部 I/F は、独自プロトコルを新規に定義するのではなく、PostgreSQLワイヤープロトコル(v3)のサブセットを実装する方式を採用する。これにより、JDBC(pgJDBC)、Python(psycopg)、Node.js(node-postgres)、.NET(Npgsql)、Go(pgx)、ODBC(psqlODBC)、PHP(PDO_PGSQL)、Ruby(pg gem)、Rust(postgres/tokio-postgres)など、各言語・各インターフェースで既に確立されたPostgreSQL用ドライバ資産が、ExecDB側の追加対応なしにそのまま接続できる。CockroachDB・YugabyteDBなど新興分散DBが採用しているのと同じ戦略である。pgx・pgJDBC・psycopg・node-postgres・Npgsql・psqlODBC・PDO_PGSQL・pg gem・Rustの9つすべてで、接続・SELECT/DML/トランザクション・DDL拒否が動くことを実機で確認済み(フェーズ④Step 5・7、フェーズ④完了後の追加検証。このリポジトリ内のtests/pgclientと、別リポジトリexecdb-drivers)。ただしNpgsqlのみ、各ドライバ自身の「デフォルト接続設定のまま」という前提が崩れる——接続文字列にServer Compatibility Mode=NoTypeLoadingの指定が必要(詳細は下記)。他の8つはこの種の指定なしに接続できる。psqlODBCはクライアント側の指定は不要だが、SQLTables/SQLColumns(テーブル一覧・列一覧を返すODBC標準API、Excel/Power BI/Access等が使う)まで動かすため、ExecDBサーバー側にPostgresシステムカタログ(pg_type/pg_class/pg_namespace/pg_attribute等)互換のビュー・関数を追加している(cmd/execdb/pgcatalog.go、詳細は.claude/rules/pgwire.md)。Rustのpostgres/tokio-postgresクレートは独自にワイヤープロトコルを再実装しており、検証の過程でExecDB本体の潜在バグ(DescribeExecuteで列のOIDが食い違う)を発見・修正した(他ドライバにも波及する修正、詳細は.claude/rules/pgwire.md)。

採用範囲(サブセット)

Postgresプロトコルの全機能を実装するのではなく、接続・クエリ実行に必要な最小限のメッセージフローに絞る。

実装する 実装しない(初期スコープ外)
認証ハンドシェイク(デフォルトtrust相当、--user指定時のみcleartext password認証。§9参照) SCRAM/MD5等の認証方式
Simple Query プロトコル(Queryメッセージ) COPY系プロトコル、LISTEN/NOTIFY
Extended Query プロトコル(Parse/Bind/Describe/Execute/Sync/Close/Flush。フェーズ④Step 5で採用に変更——理由は下記) NUMERICのバイナリ形式(複雑なため未実装。理由は下記)
結果値・パラメータ値双方のバイナリ形式(int2/int4/int8/float4/float8/bool/bytea/timestamp。理由は下記)
RowDescription / DataRow / CommandComplete 行数制限付き実行・PortalSuspendedExecuteのmaxRowsは無視し常に最後まで実行する)
エラー応答(ErrorResponse 詳細なSQLSTATEコード体系(簡略化したコードで代用)

Extended Queryを採用に変更した経緯(フェーズ④Step 1のスパイクで判明): Simple Queryのみの実装では、pgxdefault_query_exec_mode=simple_protocol)や pgJDBC(preferQueryMode=simple)の明示指定が無いと接続できず、Npgsqlに 至っては常にExtended Queryを使うため接続そのものができない——本節冒頭が 掲げる「既存ドライバ資産がExecDB側の追加対応なしにそのまま接続できる」という 目的と矛盾していたため、当初スコープ外としていたExtended Queryを採用に切り替えた (.claude/rules/pgwire.md参照)。

SQLiteは$1形式のプレースホルダをネイティブに解釈できるため (実測確認済み)、SQL文の書き換え層は不要。Describe(statement)が要求する 実行前の結果列情報は、全プレースホルダにNULLを仮バインドした試験実行 (SAVEPOINT/ROLLBACKで囲む)からColumnTypes()を読み取ることで得ており、 engine側への新規API追加は不要だった。Describe(portal、Bind後)は NULLではなく、そのポータルの実際のBind値を仮バインドに使う——SELECT $1 のようなプレースホルダそのものが結果列になる式では、実際の値を通した方が columnOIDのScanTypeフォールバックが正しい型を検出できる(real PostgreSQL 自身も、クライアントがParseで申告したパラメータ型からこの種の列の型を 決定する)。当初はstatement/portal共通でNULL仮バインドのみだったが、 フェーズ④Step 7でNpgsqlを検証した際、SELECT $1列がOID 25(text)に フォールバックしてしまい、ExecuteScalarAsync()の厳格な型キャストが失敗する 形で発覚した(pgJDBC/node-postgresは値取得API側が文字列を暗黙変換するため 表面化していなかった)。

Npgsqlが接続すらできなかった経緯・原因(フェーズ④Step 7、実機確認で判明): Extended Query採用(Step 5)によりunsupported message type 'P'は解消した はずだったが、実際にNpgsqlをデフォルト設定のまま接続すると SQL logic error: no such function: versionで失敗した。原因は、Npgsqlの 接続確立処理が独自に「型カタログのブートストラップ」を行うため—— SELECT version();に続けて、pg_type/pg_namespace/pg_class/pg_proc/ pg_range/pg_attribute/pg_enumという実在のPostgresシステムカタログを 対象にした複数のSELECTを1つのSimple Queryメッセージとしてバッチ送信して くる。SQLiteにはこれらの関数・テーブルが一切無いため、接続確立の時点で 最初の1文から失敗する。pgx/psycopg/node-postgres/pgJDBCはいずれもこの種の ブートストラップを行わない(デフォルト設定では発生しない)ため、この問題は Npgsql固有だった。対処: 接続文字列にServer Compatibility Mode=NoTypeLoading を指定する(CockroachDB・Redshift等、実物のPostgresでない互換DBに接続する 際にNpgsqlが公式に案内している標準機能——ExecDB独自のパッチではない)。 これを指定するとNpgsqlは型カタログのブートストラップ自体をスキップし、 組み込みの既知型(int4/text/bool等)だけで動作するため、この問題を 根本的に回避できる。この接続文字列パラメータは、.NETチェックの呼び出し元 (別リポジトリexecdb-drivers側) が付与しており、チェック自体にハードコードされているわけではない。

psqlODBC(ODBC)対応で追加したpg_catalog互換ビュー(フェーズ④完了後の 追加、実機確認で判明): psqlODBCは接続直後に実在のPostgresシステム カタログpg_typeへラージオブジェクト型の有無を問い合わせ、また SQLTables/SQLColumns(Excel/Power BI/Access等が使う、テーブル一覧・ 列一覧を返すODBC標準API)はpg_class/pg_namespace/pg_attribute/ pg_attrdefへの本格的なJOINクエリとpg_get_expr()/current_schema() 関数呼び出しを送ってくる。SQLiteにはこれらが一切無いため、そのままでは 接続もスキーマブラウズもできない。対処: ExecDBの実スキーマ (sqlite_masterpragma_table_info())から動的に導出する TEMPビュー・テーブルを、pgwire接続ごとに用意するcmd/execdb/pgcatalog.go)。実データを一切汚染せず(.tables/.dump/ スナップショットに現れない)、スキーマ変更後も常に最新の状態を反映する。 クライアントが送ってくるpg_catalog.pg_classのようなスキーマ修飾付き 参照は、サーバー側でpg_catalog.という文字列を除去してから実行することで 吸収している(SQLiteのVIEWは別データベースのオブジェクトを参照できない という制約があり、ATTACH DATABASE ... AS pg_catalogした独立スキーマに main参照ビューを置く案は不採用——詳細な経緯は.claude/rules/pgwire.md 参照)。current_schema()/pg_get_expr()engine.RegisterScalarFunction (新設、modernc.org/sqliteのカスタム関数登録APIの薄いラッパー)経由で 登録している。制約・インデックス・トリガー・複数スキーマは対象外—— 基本的な接続・型付きクエリ・スキーマブラウズが実スキーマに対して動く ことがゴールであり、本格的なPostgresシステムカタログの再現ではない。

結果値のバイナリ形式が必要になった経緯(フェーズ④Step 5、実機確認で判明): テキスト形式のみで実装したところ、pgxのデフォルト(Extended Query)接続で int8/float8/numeric/bool/bytea/timestamp列が軒並み失敗した (数値・日時型は明示的なエラー、bool/byteaエラーにならず黙って 誤った値を返す方が危険だった)。原因はpgxBindメッセージの resultFormatCodesで、これらの型に対しデフォルトでバイナリ形式を要求して くるため——ExecDB側でバイナリ結果値を実装しなければ、Extended Query採用の 本来の目的(デフォルト設定での接続)が数値・BLOB・日時列を含む現実的な クエリでは達成できないことが判明した。NUMERICだけは対象外とした——SQLiteの NUMERIC親和性は内部的に必ずINTEGERかREALのいずれかで格納され(真の任意 精度十進数を持たない)、Postgresの複雑なバイナリNUMERIC形式(base-10000 桁グループ符号化)を実装する代わりに、NUMERIC親和性の宣言型は実行時のGo 動的型(int64/float64)でint8/float8へ振り分ける設計とした(詳細は cmd/execdb/pgtype.go)。

パラメータ側のバイナリ形式が必要になった経緯(フェーズ④Step 7、実機確認で判明): Step 5時点では「パラメータは常にテキスト形式」と位置づけ、Bindが バイナリ形式のパラメータを受け取ると一律拒否していた(pgx/psycopgは デフォルトでパラメータをテキスト送信するため、この時点では問題が顕在化 しなかった)。ところがフェーズ④Step 7の他言語ドライバ検証で、pgJDBCが PreparedStatement.setInt/setDouble等をデフォルト設定のまま使うと、 Bindメッセージのパラメータをバイナリ形式で送ってくることが判明した (Parseメッセージ自身が、例えばsetIntに対して型OID 23/int4を 自己申告している——ParameterDescriptionがどう答えるかとは無関係に、 クライアント自身が把握している型で決め打ちしてくる)。この結果、 prepared statement経由のDML/SELECTがpgJDBCのデフォルト設定では 全滅するという、Extended Query採用の目的そのものに反する事態になったため、 Bind側もパラメータのバイナリ形式をデコードする設計に変更した。 デコードはParseメッセージがクライアント自身の申告として送ってきた パラメータ型OID(従来は読み捨てていた)を頼りに行う——Bindの ワイヤ形式自体には型情報が一切含まれないため、これが唯一の手がかりに なる。対応OIDは結果値より広く、int2/int4/float4(結果値側では columnOIDが使わないOIDだが、クライアントが申告するパラメータ型としては 現実に出現する)を含む8種(詳細はcmd/execdb/pgtype.godecodeBinaryParam)。クライアントが型OIDを申告せず(0/unspecified)に バイナリ形式で送ってきた場合は、デコードのしようがないため従来通り拒否する。

認証(オプトイン、Zero-Authがデフォルト)

ExecDBは1章の「Zero-Auth」を核心思想とするが、必要な人だけが手軽に認証を有効化できるよう、オプトインの認証機構を用意する。

  • デフォルト(--user 未指定): 常にZero-Auth。従来通り trust 相当で、パスワード不要・誰でも接続可能。
  • --user NAME 指定時のパスワード取得方法: モードを問わず、以下の優先順位で決定する。
    1. 環境変数 EXECDB_PASSWORD が設定されている場合: その値をパスワードとして使い、対話プロンプトはスキップする(REPLモード・サーバーモードのいずれでも、環境変数があれば最優先)。
    2. EXECDB_PASSWORD が未設定、かつREPLモードの場合: 起動時に標準入力からパスワードを対話的に入力させる(Password: のようなプロンプトでマスク入力、golang.org/x/termReadPassword 相当)。
    3. EXECDB_PASSWORD が未設定、かつ --no-repl(サーバーモード)の場合: 対話する相手がいないため、エラーとして起動を中止する
  • 「ユーザー」という概念は持たない: マルチユーザー管理・権限分離等は行わず、あくまで「接続に必要な1組の名前+パスワード」という単純な認証キーとして扱う。
  • 認証方式: PostgreSQLワイヤープロトコルの cleartext password 認証を採用する(SCRAM/MD5等の本格的な方式は実装しない。§8「採用範囲」参照)。

型マッピング(確定、フェーズ④Step 1〜2)

SQLiteは動的型付け(型アフィニティ)のため、列の値の型が固定されない。 RowDescriptionメッセージで返すPostgres型OIDは、列の宣言型(decltype)を 優先し、SQLite公式の型アフィニティ判定アルゴリズム(sqlite.org/datatype3.html §3.1)に沿って以下のように振り分ける。BOOLEANDATE/DATETIME/TIMEは SQLite標準の5分類には無い区分だが、modernc.org/sqliteが観測可能な形で 特別扱いする(BOOLEAN列は整数格納・DATE系列はScan結果がtime.Timeに なる)ため、標準の5分類より先に判定する。

宣言型(decltype)に含まれる部分文字列 Postgres OID 備考
BOOL bool(16) 標準5分類より優先判定
DATE または TIME timestamp(1114) 同上
INT int8(20) 標準分類「INTEGER」
CHAR / CLOB / TEXT text(25) 標準分類「TEXT」
BLOB bytea(17) 標準分類「BLOB」
REAL / FLOA / DOUB float8(701) 標準分類「REAL」
上記いずれにも該当しない(標準分類「NUMERIC」のcatch-all、例: NUMERIC/DECIMAL(10,2) 下記フォールバックへ 固定OIDを割り当てない(理由は下記)
宣言型なし(式・集約・リテラル列) 下記フォールバックへ 同上

フォールバック(実行時のGo動的型をサンプリング): 上表で確定しない列は、 先頭行を試験実行して得たScanType()の実際のGo型で振り分ける (int64int8float64float8boolbool[]bytebyteatime.Timetimestamp、それ以外・行が無い場合→text)。NUMERIC親和性の 宣言型をこの経路に回しているのは意図的な設計判断——SQLiteのNUMERIC親和性は 内部的に必ずINTEGERかREALのいずれかで格納され真の任意精度十進数を持たないため、 Postgresの複雑なバイナリNUMERIC形式(base-10000桁グループ符号化)を実装する 代わりに、int8/float8へ振り分けることでその実装自体を不要にしている。

値のテキスト/バイナリエンコードは、OIDではなくScan後の実際のGo動的型を 見て行う(宣言型と実際の値の型が食い違う——型アフィニティに違反する値が 入っている場合があるため)。詳細な実装はcmd/execdb/pgtype.gocolumnOID/affinityOID/scanTypeOID/pgEncodeValueを参照。

パラメータ(Bind)側のバイナリ形式デコードで対応するOID: int2(21)/ int4(23)/int8(20)/float4(700)/float8(701)/bool(16)/bytea(17)/ timestamp(1114)の8種(int2/int4/float4は結果値側のcolumnOIDが 一度も返さないOIDだが、クライアントが自ら申告するパラメータ型としては 独立に出現するため対応範囲に含む——詳細は下記「パラメータ側のバイナリ 形式が必要になった経緯」参照)。

アクセス制御(§2)との統合

DDLは外部I/F経由では引き続き拒否する。Postgresプロトコル上では ErrorResponse メッセージとして返す。

ErrorResponse
  Severity: ERROR
  Code:     42501  (insufficient_privilege 相当のSQLSTATEを流用)
  Message:  "DDL statements are not allowed via external interface"

SET/SHOWは、この「許可/拒否」の二分法に属さない第3の区分として扱う (§2「SET/SHOW等のセッションコマンド」参照)。外部I/F層が内部SQLエンジンへ 渡す前に横取りし、接続ごとのメモリ上のマップへ値を保持するだけで (CommandComplete("SET")SHOWは1列1行の結果)、ErrorResponseは返さない。

トランザクション(TCL)の扱い

Postgresプロトコルは接続(コネクション)自体がステートフルなため、HTTPのような無コネクション方式で必要になる「セッションID方式」は不要になる。TCPコネクション単位でそのまま BEGINCOMMIT/ROLLBACK を扱う、Postgres本来のセッション管理にそのまま準拠する。

実装: 1本のTCP/UNIX Domain Socket接続は、engineDB.Session(ctx)が返す 専有コネクション1つに対応する(6章参照)。BEGIN/COMMIT/ROLLBACKはその コネクション上へSQL文としてそのまま送られ、SQLite自身が実トランザクション として扱う。ExecDB側でトランザクション状態を再実装することはしないが、 ReadyForQueryメッセージのステータスバイトは実際の状態を反映する: 'I'(アイドル)/'T'(トランザクション中)/'E'(トランザクション中に エラーが発生し、COMMIT/ROLLBACK以外を受け付けない状態)。'E'状態で COMMIT/ROLLBACK以外の文を送ると、実行はせずErrorResponse (SQLSTATE 25P02、"current transaction is aborted")で拒否する—— 表示上のステータスだけ'E'にして実際には文を実行してしまう中途半端な 実装は、トランザクション状態を厳密に追跡するドライバ(pgx/JDBC等)を 混乱させるため採用しない。

クエリキャンセル(CancelRequest / BackendKeyData、フェーズ④Step 6)

クエリキャンセルには意図的に別々の2つの経路がある。

  1. クライアント切断時の自動キャンセル(フェーズ②Step 5から): クエリ実行中の 接続への読み取りを別goroutineで監視し、クライアントの切断(あるいは予期しない データ送出)を検知すると実行中のクエリをキャンセルする。
  2. CancelRequestプロトコル(別接続からの明示キャンセル要求): 接続確立時、 サーバーはBackendKeyDataメッセージで疑似PID・secretの組をクライアントへ 送る。クライアントは別の新しいコネクションを張り、そのPID・secretを 含むCancelRequestを送ることで、対象の接続で実行中のクエリだけを中断できる (接続自体は切断されず、以後も使い続けられる)。PID・secretが一致しない、 または対象がアイドル中(実行中のクエリが無い)の場合は無音のno-op—— 実PostgreSQL準拠。

両者は実装上、同じキャンセル機構を共有する(cmd/execdb/pgcancel.go)。 詳細・実装時に発見した設計上の注意点(context.CancelFuncの使い回しに関する 落とし穴)は.claude/rules/pgwire.md参照。

トランスポート

トランスポート 備考
TCP 標準的な接続方式。JDBC等の各種ドライバはデフォルトでこちらを使う
UNIX Domain Socket Postgres自体もローカル接続にUnixソケットを使う慣習があり、psycopg等一部ドライバはこちらにも対応。ローカル用途向けのオプション

9. 起動オプション

起動時のコマンドライン引数は、短縮フラグ(1文字)と長いフラグの両方を用意する。短縮フラグは大文字小文字の混在による覚えにくさを避けるため、全て小文字で統一する。

短縮 長い形式 デフォルト 役割
-p --pg-addr string ""(無効) PostgreSQL互換ワイヤープロトコルのTCP待受アドレス(例: :5432127.0.0.1:5432)。未指定なら外部I/Fを起動しない
-s --socket string ""(無効) 同プロトコルをUNIX Domain Socket経由でも待ち受ける場合のパス(例: /tmp/execdb.sock)。未指定ならSocket I/Fを起動しない
-u --user string ""(無効、Zero-Auth) 指定すると外部I/Fの接続に認証キー(名前+パスワード)を要求する(§8参照)。パスワードは環境変数 EXECDB_PASSWORD があればそれを使用、なければREPLモード時のみ起動時に対話入力させる(--no-replかつ未設定の場合はエラー)
-o --snapshot-as string (未指定) .snapshot実行時、および--no-repl(サーバーモード)終了時の自動保存(下記参照)で使うデフォルトファイル名
-n --no-repl bool false REPLを起動せず、外部I/Fのみでバックグラウンド常駐する(サーバーモード)
-q --quiet bool false 起動時のバナー・ログ出力を抑制する
-t --timestamp bool false 保存時にファイル名へタイムスタンプを付与するか(下記参照)
-i --snapshot-interval duration 0(無効) 指定間隔で自動的に別名スナップショット保存を行う(例: 5m, 1h)。REPLモード・サーバーモードいずれでも有効
-h --help bool - ヘルプ表示

--pg-addr--socket はどちらも省略可能・併用可能。両方省略した場合はREPL単体(外部I/Fなし)で起動する。

タイムスタンプ付与

.snapshot 実行時、ファイル名に日時タイムスタンプ(_YYYYMMDDHHMMSS)を付与するかどうかを --timestamp-t)で切り替えられる。boolフラグであり、値の選択肢は持たない。

  • --timestamp を指定した場合: 以下のルールでファイル名を生成する(ファイル名省略時・明示指定時、CLI起動オプション・REPLの.snapshotコマンドのいずれでも共通のルール)。

    1. ベースとなるファイル名(拡張子を除く部分)から、既存の _YYYYMMDDHHMMSS パターンがあれば取り除く(二重付与を防ぐ)。
    2. 拡張子の直前に新しい _YYYYMMDDHHMMSS を挿入する。
    3. Windows環境で拡張子が省略されている場合は .exe を付与する。
    ベースとなるファイル名 生成結果
    mydb(ファイル名省略、実行中バイナリ名がベース) mydb_YYYYMMDDHHMMSS
    mydb_20260101120000(既存タイムスタンプあり) mydb_YYYYMMDDHHMMSS(古いものは除去し差し替え)
    mydb.exe mydb_YYYYMMDDHHMMSS.exe
    mydb_20260101120000.exe mydb_YYYYMMDDHHMMSS.exe
    mydb-o mydb で明示指定、Windows環境) mydb_YYYYMMDDHHMMSS.exe(拡張子省略時は.exeを付与)
  • --timestamp を指定しない場合(デフォルト): タイムスタンプは付与せず、指定されたファイル名(またはWindowsでは拡張子.exeを補完したファイル名)をそのまま使う。同名ファイルへの上書きが起きうる。

    ベースとなるファイル名 生成結果
    mydb mydb(Linux/macOS)/mydb.exe(Windows)
    mydb.exe mydb.exe

このオプションは起動時のデフォルト値として機能するが、REPLの .snapshot コマンド実行時にも同様のオプションでその場で上書き指定できる。

.snapshot bug_123 --timestamp

.overwrite にはファイル名指定の概念がないため(自分自身のパスに固定)、--timestamp オプションは適用されない。

サーバーモード(--no-repl)の停止・保存

--no-repl で起動した場合、REPLが存在しないため .snapshot / .overwrite コマンドを打つ手段がない。代わりに、終了シグナル受信時に自動でスナップショット保存を行ってから終了する方式を採る。

  • 保存方法: SIGTERM / SIGINT を受信すると、--snapshot-as / --timestamp の設定に従って(別名ファイルとして)自動保存してからプロセスを終了する。

  • REPLモードとの方針の違い: §1・§4で「保存は明示操作のみ、自動保存なし」としている原則は REPLモードにおける方針である。サーバーモードでは .snapshot を打つ手段自体が存在しないため、終了シグナルを「保存の引き金」として扱う。この違いはサーバーモード固有の例外として扱う。

  • プラットフォーム別の対応状況:

    OS 対応状況
    Linux / macOS 標準的な SIGTERM / SIGINT ハンドリングで対応(docker stop、systemd等からの終了要求を含む)
    Windows(コンソールにアタッチした状態) Ctrl+C 等のコンソール制御イベントが Go の os/signal を通じて SIGINT 相当に変換されるため、対応可能
    Windows(Windows Serviceとしてのヘッドレス常駐) v1のスコープ外(既知の制限事項)。 コンソールを持たないため制御イベントが届かず、正常な自動保存・終了は保証されない。将来的に必要であれば golang.org/x/sys/windows/svc によるService Control Manager統合を別途検討する

定期スナップショット(--snapshot-interval

--snapshot-interval-i)を指定すると、その間隔で自動的に別名スナップショット保存を繰り返す。REPLモード・サーバーモードのどちらでも有効。

  • 用途: REPLモードでは「長時間の対話セッション中に保存し忘れてクラッシュし、全データを失う」事故を防ぐ保険機能として、サーバーモードでは定期バックアップとして利用できる。§1の「保存は明示操作のみ」という原則に対する、オプトインの例外機能という位置づけ。
  • 保存方式: 別名保存(.snapshot相当)のみに対応する。自己上書き(.overwrite相当)の定期実行は、実行中ファイルへの頻繁な書き換えによるリスクが高いため、サポートしない。
  • ファイル名: --snapshot-as / --timestamp の設定に従う。--timestamp を指定した場合はタイムスタンプ付きファイルが間隔ごとに生成され続けるため、ファイルの肥大化・整理は §4 と同様に運用側(ユーザー)に委ねる。固定ファイル名で上書きし続けたい場合は --timestamp を指定しない(デフォルト)。

10. ライフサイクル・動作フロー

[1. 起動]
   └─> ./execdb を実行
   └─> バイナリ末尾からデータを読み込み、メモリ(RAM)へ展開
   └─> (`--user` 指定時のみ)パスワードを決定(§8, §9参照)
        ├─> `EXECDB_PASSWORD` 環境変数が設定されていればそれを使用(対話プロンプトなし)
        ├─> 未設定 かつ REPLモード: 標準入力から対話的にパスワード入力させる
        └─> 未設定 かつ `--no-repl`(サーバーモード): エラーで起動中止
   └─> バックグラウンドで外部 I/F (PostgreSQL互換ワイヤープロトコル) を起動
   └─> 起動時バナーを表示(下記参照。`-q`/`--quiet` で抑制可能)
   └─> フォアグラウンドで「対話式コンソール (REPL)」を開始(`--no-repl` 時はここでサーバーモードへ移行)

[2. 運用・クエリ実行]
   ├─> 対話コンソール : DDL / DML / TCL のすべてを実行可能
   └─> 外部 I/F      : DML / TCL のみを受け付け(DDLは ErrorResponse で弾く)

[3. 保存(別名)]
   └─> .snapshot コマンドを実行(明示操作のみ、自動保存なし)
   └─> メモリ上の最新状態 + エンジン本体をパッキング
   └─> 新しい別名実行ファイル(例: execdb_20260831_150000)を生成して出力

[3'. 保存(自己上書き)]
   └─> .overwrite コマンドを実行(明示操作のみ)
   └─> 自分自身を <path>.execdb_old へ退避 → 空いた元のパスへ新しい中身を書き込み(§7)
   └─> 成功したらそのままREPLを終了(この操作のみ、保存と終了が同時に起きる。§4参照)

[4. 停止(REPLモード)]
   └─> 通常の終了手段: .exit / .quit コマンド、または標準入力のEOF(Ctrl+D)
   └─> Ctrl+C(SIGINT)は対話端末では終了ではなく中断コマンドとして扱う(sqlite3のシェル準拠):
        ├─> クエリ実行中に押すと、そのクエリだけを中断してプロンプトへ戻る(プロセスは終了しない)
        ├─> アイドル時(クエリ非実行中)に1回押すと、入力中の複数行分の未確定なSQLを破棄して
        │    プロンプトを出し直すだけで、プロセスは終了しない
        ├─> アイドル時に間に新しい入力行を挟まず連続で2回押すと、そこでプロセスを終了する
        │    (終了コード1。新しい入力行を1行でも読むと連続回数はリセットされる)
        └─> 標準入力が対話端末でない場合(パイプ・スクリプト実行時)はこのハンドラを一切
             登録せず、SIGINTは通常のプロセス終了として扱われる(スクリプトから起動した
             ExecDBをCtrl+Cで止められなくなる事故を防ぐため)
   └─> SIGTERMを受信した場合(対話モードでは自動保存の対象外。サーバーモードとの違いは
        [4'.]参照)はプロセスを終了する
   └─> いずれの終了経路でも、メモリ上のデータは保存されず消える(揮発性が前提。保存したい
        場合は終了前に .snapshot または .overwrite を実行する)

[4'. 停止(サーバーモード、--no-repl)]
   └─> SIGTERM / SIGINT(Windowsのヘッドレス常駐時は対象外。§9参照)を受信
   └─> --snapshot-as / --timestamp の設定に従い自動でスナップショット保存(別名)
   └─> 保存完了後にプロセスを終了

起動時バナー

起動時、REPL・外部I/Fが利用可能になったことを示すバナーを表示する。表示内容は バージョン、埋め込みデータの有無・スナップショット名、外部I/Fの待受状況(指定 されている場合のみ)、--user認証が有効かどうか(パスワード自体は表示しない)、 サーバーモードかどうか。.statusのような専用コマンドは設けず、起動時にこの バナーで一度提示することに一本化する。-q/--quiet指定時はバナー全体を抑制する。

REPLモード、外部I/Fあり:

ExecDB v0.1.1
Loaded snapshot: mydb_20260901120000
Listening on :5432 (PostgreSQL wire protocol)
Listening on /tmp/execdb.sock (UNIX Domain Socket)
Enter ".help" for usage hints.

REPLモード、データなし・外部I/Fなし:

ExecDB v0.1.1
No embedded data. Starting with an empty in-memory database.
Enter ".help" for usage hints.

サーバーモード(--no-repl):

ExecDB v0.1.1
Loaded snapshot: mydb_20260901120000
Listening on :5432 (PostgreSQL wire protocol)
Running in server mode (--no-repl). Send SIGTERM to save and exit.