# 資料庫變更先經主管核准

> 查詢馬上執行；每項變更要等主管在手機上看過確切的 SQL 和預計影響筆數並核准後，才執行一次。資料庫始終留在你自己的伺服器上。

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

讓員工、腳本或助理直接查資料庫，能省下好幾個小時；讓他們不經過任何人就改資料，一句「清掉舊購物車」就可能刪掉上週的訂單。Hibira 在兩者之間放了一位主管：查詢馬上執行，每一項變更都要等主管在手機上看過確切的 SQL 和會影響幾筆資料、按下核准後才執行。你的資料庫和連線密碼始終留在自己的伺服器上。

## 使用者得到什麼

- 唯讀的問題馬上有答案，不用等任何人。
- 每項變更都有一個核准頁面，主管可以從推播通知或 QR code 打開：上面有 SQL、預計影響筆數、執行計畫和指紋，以及「核准」和「拒絕」。
- 主管一做決定，發起人的畫面馬上顯示結果，不用重新整理。被拒絕時會附上主管的理由。
- 保證執行的就是被核准的那一條 SQL，而且只執行一次。

![demo 主控台：一條 DELETE 正在等主管核准，旁邊有 QR code、預計影響筆數和 SQL 的 SHA-256。](https://hibira.io/portal/articles/sql-approvals-console-zh-TW-light.webp)

## 用到的功能

- **[審批](https://hibira.io/zh-tw/guides/approvals)**：`POST /apps/APP_ID/approvals`（或 [MCP server](https://hibira.io/zh-tw/mcp-server) 的 `request_approval`）會建立一個 Hibira 代管的核准頁面，並在頻道上通知決定結果。
- **私有頻道**：每個主控台或每次執行一個，例如 `private-sqlgate-<random>`，`approval` 事件會送到這裡，不用輪詢。
- **推播通知**（選用）：把連結送到主管的手機。
- **[@hibira-io/approvals/sql](https://hibira.io/zh-tw/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 code，然後按核准。

## 步驟 1：查詢放行，其他一律攔下

`classifySql()` 只有在確定的時候才把一條 SQL 當成查詢：只有一條語句、開頭是 `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` 完全不會執行；在會回滾的交易裡試跑能拿到精確的筆數，但語句確實會跑一次，所以要設逾時。再對 SQL 原文算出指紋：這次核准只對這一條 SQL 有效。

```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：送出核准

建立審批時，把 SQL 和預覽放在 `details`，並指定要通知結果的頻道。SQL 和指紋留在你這邊，用審批 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 code），在以自己語言顯示的頁面上回覆。

![主管手機上的核准頁面：SQL、預計影響筆數、SHA-256，以及核准和拒絕。](https://hibira.io/portal/articles/sql-approvals-page-zh-TW.webp)

## 步驟 4：結果一到，只執行一次

決定會以 `approval` 事件送到頻道上。把它當成門鈴：先從 API 讀一次結果，確認存下來的 SQL 和指紋仍然相符，再執行。先「認領」這一列，就算兩個事件或兩個 worker 同時進來，也只會執行一次：

```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-zh-TW-light.webp)

## 已經用 MCP server 連資料庫了？

如果團隊是在 Claude、Cursor、ChatGPT 或其他 MCP 用戶端裡，透過 DBHub、postgres-mcp、Supabase MCP 這類 server 操作資料庫，只要在前面加上 [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate)，一行程式都不用改。查詢直接通過；每一筆新增、更新、刪除或結構變更，都會停在同一個核准頁，主管核准後才照原樣執行一次。

```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-zh-TW.mp4)

## 在你用的工具裡做同樣的把關

- **Vercel AI SDK、Mastra、LangGraph（JavaScript 和 Python）**：[@hibira-io/approvals](https://hibira.io/zh-tw/guides/approvals) 每一種都有範例：把 `needsApproval` 或 `requireApproval` 寫成依 SQL 判斷的函式，並在你自己的應用程式裡用審批收件匣顯示每個呼叫的預覽。
- **Laravel**：[filament-approvals](https://hibira.io/zh-tw/guides/filament-approvals) 直接在 Filament 後台裡審批，README 裡有給 Laravel AI SDK 用的 `RunSql` 工具。
- **[n8n](https://hibira.io/zh-tw/guides/n8n)、[Make](https://hibira.io/zh-tw/guides/make)、[Activepieces](https://hibira.io/zh-tw/guides/activepieces) 和 Dify**：用程式碼步驟判斷 SQL 類型，Hibira 的審批步驟請主管決定並等待結果，資料庫步驟只接在「核准」的分支上。每個的 README 都有完整流程。
- **Claude、Cursor 和其他 MCP 用戶端**：[MCP server](https://hibira.io/zh-tw/mcp-server) 的 `request_approval` 也能在對話中用同樣的方式送審。
- **任何資料庫 MCP server**：[mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) 不必改程式，就能包住 DBHub、postgres-mcp、Supabase MCP 等 server。
