# データベースの変更に上長の承認を

> 参照はすぐに実行。変更は正確な SQL と影響する見込みの行数を上長がスマホで確認して承認してから、一度だけ実行します。データベースは自社のサーバーから出ません。

Source: https://hibira.io/ja/use-cases/database-write-approvals

スタッフやスクリプト、アシスタントにデータベースを直接参照させると、何時間もの手間が省けます。でも誰の確認もなしに変更させると、「古いカートを片付けて」の一言で先週の注文が消えることもあります。Hibira はその間に上長を置きます。参照はすぐに実行し、変更はひとつずつ、上長がスマートフォンで正確な SQL と影響する行数を確認して承認するまで待ちます。データベースも接続情報も、自社のサーバーから出ることはありません。

## ユーザーが得られるもの

- 読み取りだけの質問には、誰も待たずにすぐ答えが返ります。
- 変更ごとに承認ページがあり、上長はプッシュ通知や QR コードから開けます。SQL、影響する見込みの行数、実行計画、フィンガープリントと、「承認」「却下」が並びます。
- 上長が決めた瞬間に、依頼した人の画面に結果が出ます。再読み込みは不要で、却下なら上長の理由も届きます。
- 実行されるのは承認されたものとまったく同じ文で、実行は一度だけです。

![デモのコンソール：DELETE が上長の承認を待っていて、QR コード、影響する見込みの行数、SQL の SHA-256 が表示されています。](https://hibira.io/portal/articles/sql-approvals-console-ja-light.webp)

## 使う機能

- **[承認](https://hibira.io/ja/guides/approvals)**：`POST /apps/APP_ID/approvals`（または [MCP サーバー](https://hibira.io/ja/mcp-server)の `request_approval`）が Hibira ホストの承認ページを作り、決定をチャンネルで知らせます。
- **プライベートチャンネル**：コンソールや実行ごとにひとつ、たとえば `private-sqlgate-<random>`。ここに `approval` イベントが届くので、ポーリングは不要です。
- **プッシュ通知**（任意）：上長のスマートフォンにリンクを届けます。
- **[@hibira-io/approvals/sql](https://hibira.io/ja/guides/approvals)**：Node.js 用の `classifySql()`、`sqlFingerprint()`、`assertApprovedSql()`。Python にも同じものがあります。

```text
sequence: 参照はすぐ実行、変更は上長が承認してから実行
依頼者 | 自社サーバー | *Hibira | 上長
1 > 2: SELECT … | 読み取り専用で実行、すぐに結果
1 > 2: DELETE FROM carts … | 試し実行：10 行
2 > 3: POST /approvals | SQL、見込み行数、SHA-256
3 > 4: プッシュ通知 | 承認ページへのリンク
4 > 3: 承認
3 > 1: approval イベント | プライベートチャンネル、ポーリングなし
1 > 2: 実行して
2 > 3: GET /approvals/apr_… | 回答を一度だけ確認
2 >> 1: 一度だけ実行：10 行を変更
```

[demos.hibira.io](https://demos.hibira.io/examples/nextjs-sql-approvals/) で試せます。変更を選び、スマートフォンで QR コードを読み取って承認してください。

## ステップ 1：参照は通し、それ以外は止める

`classifySql()` が参照とみなすのは確信できるときだけです。文がひとつで、`SELECT`、`WITH`、`EXPLAIN`、`SHOW` で始まり、データを変える部分がないもの。複数の文、`WITH ... DELETE`、`EXPLAIN ANALYZE`、`SELECT ... INTO`、`FOR UPDATE`、確実に解析できないものはすべて変更として扱います。それでも参照は読み取り専用のトランザクションで実行しましょう。判定を誤った文があっても、書き込まずに失敗するだけです。

```js
import pg from 'pg';
import { classifySql } from '@hibira-io/approvals/sql';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export async function runSql(sql, requestedBy) {
  if (classifySql(sql) === 'read') return { rows: await readOnly(sql) };
  return { approval: await askSupervisor(sql, requestedBy) };
}

async function readOnly(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN READ ONLY');
    const { rows } = await db.query(sql);
    await db.query('COMMIT');
    return rows.slice(0, 200);
  } catch (err) {
    await db.query('ROLLBACK');
    throw err;
  } finally {
    db.release();
  }
}
```

## ステップ 2：変更が何をするかを上長に見せる

依頼する前に行数を見積もります。`EXPLAIN` は文を実行しません。ロールバックするトランザクションでの試し実行は正確な件数が出ますが、文は実際に走るのでタイムアウトを付けます。そして文そのもののフィンガープリントを取ります。承認はこの文にだけ有効です。

```js
import { sqlFingerprint } from '@hibira-io/approvals/sql';

async function preview(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN');
    await db.query("SET LOCAL statement_timeout = '5s'");
    const { rowCount } = await db.query(sql);
    return { rows: rowCount, fingerprint: await sqlFingerprint(sql) };
  } finally {
    await db.query('ROLLBACK');
    db.release();
  }
}
```

## ステップ 3：承認を依頼する

文とプレビューを `details` に入れ、決定を知らせるチャンネルを指定してリクエストを作ります。文とフィンガープリントは承認 ID と結びつけて自社側に保存します。Hibira に送るのはリクエストの文面だけで、接続情報も行データも送りません。

```js
import Pusher from 'pusher';

const hibira = new Pusher({ appId: process.env.HIBIRA_APP_ID, key: process.env.HIBIRA_KEY, secret: process.env.HIBIRA_SECRET, host: 'ws.hibira.io', useTLS: true });

async function askSupervisor(sql, requestedBy) {
  const { rows, fingerprint } = await preview(sql);
  const res = await hibira.post({
    path: '/approvals',
    body: {
      title: 'Run this database change?',
      details: `${sql}\n\nEstimated rows: ${rows}\n\nSHA-256: ${fingerprint}`,
      options: ['Approve', 'Reject'],
      channel: `private-sqlgate-${requestedBy.consoleId}`,
      push_users: ['supervisor-42'],
      expires_in_minutes: 15,
      agent: 'SQL console',
    },
  });
  const approval = await res.json();
  await pool.query('INSERT INTO pending_changes (approval_id, sql, fingerprint) VALUES ($1, $2, $3)', [approval.approval_id, sql, fingerprint]);
  return { id: approval.approval_id, url: approval.url, rows };
}
```

上長にはリンク付きのプッシュ通知が届き（または依頼者の画面の QR コードを読み取り）、自分の言語で表示されたページで回答します。

![上長のスマートフォンに表示された承認ページ：SQL、影響する見込みの行数、SHA-256、承認と却下。](https://hibira.io/portal/articles/sql-approvals-page-ja.webp)

## ステップ 4：回答が届いたら一度だけ実行する

決定はチャンネルに `approval` イベントとして届きます。これは呼び鈴として扱いましょう。API から回答を一度読み、保存した文がフィンガープリントと一致することを確かめてから実行します。先に行を確保しておけば、イベントやワーカーが重なっても実行は一度だけです。

```js
import { assertApprovedSql } from '@hibira-io/approvals/sql';

// In the requester's page (pusher-js), or a worker that keeps a connection open:
channel.bind('approval', ({ id, reason }) => {
  if (reason === 'decided') fetch(`/api/changes/${id}/run`, { method: 'POST' });
});

// POST /api/changes/:id/run on your server:
export async function runApproved(id) {
  const res = await hibira.get({ path: `/approvals/${id}` });
  const answer = await res.json();
  if (answer.status !== 'approved') return { status: answer.status, comment: answer.comment };
  const { rows } = await pool.query('UPDATE pending_changes SET ran_at = now() WHERE approval_id = $1 AND ran_at IS NULL RETURNING sql, fingerprint', [id]);
  if (!rows.length) return { status: 'already run' };
  await assertApprovedSql(rows[0].sql, rows[0].fingerprint);
  const result = await pool.query(rows[0].sql);
  return { status: 'ran', rows: result.rowCount };
}
```

![承認後：コンソールに、変更が一度だけ実行され何行が変わったかが表示されています。](https://hibira.io/portal/articles/sql-approvals-done-ja-light.webp)

## すでに MCP サーバー経由でデータベースを使っていますか？

Claude、Cursor、ChatGPT などの MCP クライアントから、DBHub、postgres-mcp、Supabase MCP といったサーバー経由でデータベースを扱っているなら、その前に [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) を置くだけです。コードの変更は要りません。読み取りはそのまま通り、INSERT・UPDATE・DELETE やスキーマ変更は同じ承認ページで止まり、承認されたとおりに一度だけ実行されます。

```sh
npx -y @hibira-io/mcp-approval-gate --label shop-production --push-interests db-approvals \
  -- npx -y @bytebase/dbhub --transport stdio --dsn "$DATABASE_URL"
```

![実際の操作：注文 1042 を返金済みにする依頼がいったん止まり、上長がスマートフォンで UPDATE を承認すると、変更が一度だけ実行されます。](https://hibira.io/portal/articles/mcp-approval-gate-ja.mp4)

## お使いのスタックで同じゲートを

- **Vercel AI SDK、Mastra、LangGraph（JavaScript と Python）**：[@hibira-io/approvals](https://hibira.io/ja/guides/approvals) にそれぞれのレシピがあります。`needsApproval` や `requireApproval` を文で判定する関数にし、自分のアプリの承認インボックスで呼び出しごとのプレビューを表示します。
- **Laravel**：[filament-approvals](https://hibira.io/ja/guides/filament-approvals) は Filament パネルの中で承認を取ります。README に Laravel AI SDK 用の `RunSql` ツールがあります。
- **[n8n](https://hibira.io/ja/guides/n8n)、[Make](https://hibira.io/ja/guides/make)、[Activepieces](https://hibira.io/ja/guides/activepieces)、Dify**：コードのステップで文を判定し、Hibira の承認ステップで上長に依頼して待ち、データベースのステップは承認の分岐にだけつなぎます。それぞれの README にワークフローがあります。
- **Claude、Cursor などの MCP クライアント**：[MCP サーバー](https://hibira.io/ja/mcp-server)の `request_approval` で、チャットから同じように依頼できます。
- **あらゆるデータベース MCP サーバー**：[mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) なら、DBHub、postgres-mcp、Supabase MCP などをコードの変更なしで包めます。
