· 8 分で読めます

データベース GUI が直接書き込むべきでない理由

どんなデータベース GUI にも、クリックが本番への書き込みに変わる瞬間があります。私たちは、その瞬間は存在すべきでないと決めました。DynoTable では、あなたがすること — アイテムの編集も、一括削除も、による変更も — が DynamoDB に直接触れることはありません。すべての書き込みは、属性ごとのレビュー可能な差分としてに着地し、人間がコミットしたときにのみ送られます。

私たちが目指した心的モデルは git です — レビューのためのステージングエリアと、git push --force-with-lease のように振る舞うコミット、つまりリモートがあなたが最後に見たときのままである場合にのみ成功する、というものです。この記事はその土台にあるエンジニアリングについてです — 設計全体を作り変えたタブをまたぐバグ、主な成果が 削除されたコード だったリファクタリング、そして AI エージェントの到来が UX 上の気の利いた工夫を、耐力を担う安全の壁へと変えた理由です。

書き込みは差分である

編集はローカルの SQLite に支えられたステージに蓄積され、サイドパネルに差分カード — 属性ごとの古い値と新しい値 — として表示されます。グリッドでは行が色づくので、ステージされた状態はパネルだけでなくデータからも見えます。コミットは、ステージされた集合を DynamoDB のとして、サービスの上限に合わせてチャンク分割し(トランザクションあたり 100 アイテム、操作ごと・リクエストごとのバイト予算つき)、チャンクごとに 30 秒のタイムアウトで、順次送信します。

保留中の変更を属性ごとの差分カードとして表示するステージングパネル。行ごとのコミットと一括コミットに対応。
保留中の変更を属性ごとの差分カードとして表示するステージングパネル。行ごとのコミットと一括コミットに対応。

ステージングのドキュメントは日々のワークフローを扱います。そこで扱われていないのは、ステージが 何をキーにしているか — これがシステムの中で最も重大な決定だと判明しました。

タブではなく、テーブルにキー付けする

最初のバージョンは、各ステージをそれを作成したタブにスコープしていました。その設計は本物のバグを生みました — AI のステージングツールは、その対象を アクティブなタブ から導出し、そのガードはテーブルビューではないタブをスキップしていたのです。そのため、SQL ワークベンチのタブにフォーカスがある状態でアシスタントに行の修正を頼むと、その編集はワークベンチのステージにステージされ — そして「ステージした変更を表示」チップは間違ったタブを開きました。

さらに悪いことに、コミットロックもタブごとでした。同じテーブル を表示している 2 つのタブは、独立した 2 つのロックを保持し — つまり両方が、同じステージされた行を本物の DynamoDB に同時にコミットできてしまいました。本番に対する二重コミットの競合が、データモデルに組み込まれていたのです。

修正は、すべてをタブではなく テーブルアイデンティティ{profile, region, tableName} — によってキー付けし直すことでした:

  • テーブルにつき 1 つのステージ。そのテーブルのあらゆるビューから見え、タブを閉じて開き直しても生き残ります。AI はテーブルのタブが 1 つも開いていなくてもステージできます。
  • テーブルにつき 1 つのコミットロック。二重コミットの競合は「処理される」のではなく — 表現不可能になります。
  • 間違ったタブというバグのクラスは 構造上 消えます — キーの中にタブが存在しないからです。

そしてキー付けのし直しは、追加したよりも多くのコードを削除しました。閉じたタブのステージをガベージコレクトする起動時のスイープ? あるレビュアーは、新しいモデルの下ではそれが機能を破壊するものだと指摘しました — ステージはもうタブとともに死なないからです — そのため丸ごと削除されました。タブを閉じたときの破棄と、その確認ダイアログも削除。残る唯一の本物の孤児は、削除された接続プロファイルであり、これは明示的にカスケードします。

マイグレーション自体は、リポジトリで初めて行を 変更する ものでした — 手書きの SQLite テーブル再構築で、レガシーなタブごとの行を ROW_NUMBER() OVER (PARTITION BY …) ウィンドウ(最新の編集が勝つ)で重複排除し、新しいキーを char(0) セパレーターで、ランタイムのキービルダーとバイト単位で同一になるように埋め戻しました。行を変更するマイグレーションは、その日、専用の seed してからマイグレートするテストハーネスを手に入れました。

DynamoDB のための --force-with-lease

レビューした対象が、書き込まれる対象と違うなら、レビュー UI は無価値です。ステージングとコミットのあいだに、他の誰かがその行を変更しているかもしれません。そこですべてのコミット操作は、書き込みをあなたがレビューした正確なスナップショットに固定する — 属性ごとの — を伴います:

  • 作成 は、そのアイテムがまだ存在しないことをアサートします。
  • 更新 は、変更しようとしているすべての属性が、ステージしたときに見た値をまだ保持していることをアサートします。
  • 削除 ですら、その属性がまだ古い値と等しいことをアサートします — あなたの note の削除がステージされているあいだに、チームメイトが note"old" から "new" に変更していたなら、コミットが彼らの編集を静かに削除してはなりません。

条件が失敗すると、DynamoDB はトランザクション全体をキャンセルし、どのアイテムがずれたのかを教えてくれます。その行には、どちらの方向であれ静かな上書きではなく、コンフリクトバナー — ライブの値に対してリベースするか、中止するか — が表示されます。

そしてチャンクのいずれかが失敗すると、コミッターは 停止 します。コミット済みのチャンクはコミットされたまま、失敗したチャンクはアトミックにロールバックされ、それ以上は何も試みられません。私たちはベストエフォートの継続をあえて拒否しました — コンフリクトを越えて書き込みを続けるレビューツールは、誰もレビューしていない変更セットを適用しているからです。

human reviews diffsTransactWriteItems +per-attribute conditionscondition failedrebaseEdit / AI stageItemStaging areaper tableCommitDynamoDBConflict banner:rebase or abort

サブシステムを削除したリファクタリング

コミットは長時間実行されます — アプリは各コミットをリプレイレジストリに登録するので、レンダラーのリロード — あるいは 2 つ目のウィンドウ — が再アタッチして、それが完了するのを見届けられます。時が経つにつれ、3 つの異なるコード経路がそれぞれコミットごとに 2 つ のオブザーバーをアタッチするようになり、1 つの問いを巡って調停の仕組みがまるごと育ちました — どのオブザーバーがコミットロックを解放してよいのか? それには独自の恐ろしい名前(ownsLockOnBeforeRegistration)、IPC の順序競合を説明するコメントブロック、そして 230 行のトーストトラッカーがありました。

修正は単一の所有者でした — コミットごとにちょうど 1 回だけアタッチし、ロックを排他的に所有し、進捗をストアに投影し、その start 呼び出しがあらゆる終了経路で決着する — 決してハングせず、決して reject しない — コミットセッション です。調停の仕掛けとトラッカーは移設されたのではなく、削除されました。ステージングパネルを酷使するメモリベンチマークは、ヒープの約 3 分の 1 を減らしました。その月に受け取った最高のコードレビューコメントは — 「この PR はほとんど赤だ」でした。

その所有権モデルから、本番に書き込むあらゆるツールにとって必須だと私たちが考える 2 つの振る舞いが導かれます — 進行中のコミットは レンダラーのリロードを生き延び(セッションが再アタッチして完了する)、そしてコミット途中でライセンスが読み取り専用に切り替わっても生き延びます — 新しいコミットはブロックされますが、すでに進行中の書き込みは最後まで見届けられ、半分適用されたまま放棄されることは決してありません。

ワイヤーフォーマットがあえて醜い理由

ステージされた値は、扱いやすい unmarshal 済みの JSON ではなく、生の DynamoDB の型付きエンベロープ — {N: "42"}{S: "42"} — として保存されます。理由は 2 つあります。差分は 型を意識 しなければなりません — {N: "1"}{S: "1"} は異なる値であり、unmarshal 済みの値の上に構築されたプライマリキーのハッシュは、それらを衝突させてしまいます。そしてラウンドトリップはロスレスでなければなりません — SDK の unmarshal 済みの数値ラッパーは、SQLite への JSON シリアライズと復元を生き延びられず、ユーザー入力との深い等価判定が壊れます。コミット経路が生のクライアントを使うのも同じ理由です — あなたがレビューしたものが、条件に使われ書き込まれるものと、バイト単位で同一なのです。

そしてエージェントが到来した

ステージングは私たちの AI 機能より前から存在しますが、それらが出荷できた理由でもあります。アシスタントの書き込みツールはステージします — そのツール自身の説明が、モデルに一字一句こう伝えます:

The user reviews + commits from the staging panel —
this tool never writes to DynamoDB directly.

コミットツールは存在しません。権限でゲートされているのでも、監査されているのでもなく — 不在 です。MCP サーバーは同じ境界を構造的に公開します — その中間の consent スコープは、文字通り「read + stage」と名付けられています。そのスコープのを受けたエージェントはゴミを提案できますが、その爆発半径は人間が読む 1 枚の差分カードです。そしてコミットは属性ごとの楽観的ロックを伴うので、古くなったエージェントの編集ですら、同時進行の人間の編集を静かに壊すことはできません — 他のすべてと同じように、コンフリクトとして表面化します。

私たちはエージェントのクエリを信頼できるものにすることについて書きました — ステージングはそのもう半分、つまりその 書き込み を退屈なものにすることです。

あなたのデータベースに書き込むあらゆるツールに要求すべきこと

  • 意図と書き込みのあいだのレビューステップ — 確認ダイアログではなく、差分。
  • レビューしたスナップショット に対する楽観的並行制御 — 削除や除去も含め、決して last-writer-wins にしない。
  • 失敗したら停止するアトミックなバッチ — コンフリクトを越えたベストエフォートの継続ではなく。
  • クラッシュやリロードを生き延び、半分適用された集合を残さない書き込み経路。
  • AI 向けには — エージェントが持つ 唯一 の書き込みプリミティブとしてのステージング。欠けている機能は、ゲートされた機能に勝ります。

ステージングのドキュメントはワークフローを示し、DynamoDB データの編集はそれが守る基本を扱います。あるいはDynoTable をダウンロードして⌘S でいくつか編集をステージし、一括変更が、それが起こる前に実際に読めるものになる様子を見てください。

Console なしで DynamoDB を扱う

DynoTable は DynamoDB 向けの高速なデスクトップクライアントです — テーブルを閲覧し、SQL スタイルのクエリを実行し、アイテムをローカルで編集できます。