# 数据库变更先经主管批准

> 查询马上执行；每项变更要等主管在手机上看过确切的 SQL 和预计影响行数并批准后，才执行一次。数据库始终留在你自己的服务器上。

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

让员工、脚本或助手直接查数据库，能省下好几个小时；让他们不经过任何人就改数据，一句“清掉旧购物车”就可能删掉上周的订单。Hibira 在两者之间放了一位主管：查询马上执行，每一项变更都要等主管在手机上看过确切的 SQL 和会影响多少行、点了批准后才执行。你的数据库和连接密码始终留在自己的服务器上。

## 用户得到什么

- 只读的问题马上有答案，不用等任何人。
- 每项变更都有一个批准页面，主管可以从推送通知或二维码打开：上面有 SQL、预计影响行数、执行计划和指纹，以及“批准”和“拒绝”。
- 主管一做决定，发起人的页面马上显示结果，不用刷新。被拒绝时会附上主管的理由。
- 保证执行的就是被批准的那一条 SQL，而且只执行一次。

![demo 控制台：一条 DELETE 正在等主管批准，旁边有二维码、预计影响行数和 SQL 的 SHA-256。](https://hibira.io/portal/articles/sql-approvals-console-zh-CN-light.webp)

## 用到的功能

- **[审批](https://hibira.io/zh-cn/guides/approvals)**：`POST /apps/APP_ID/approvals`（或 [MCP server](https://hibira.io/zh-cn/mcp-server) 的 `request_approval`）会创建一个 Hibira 托管的批准页面，并在频道上通知决定结果。
- **私有频道**：每个控制台或每次执行一个，例如 `private-sqlgate-<random>`，`approval` 事件会送到这里，不用轮询。
- **推送通知**（可选）：把链接送到主管的手机。
- **[@hibira-io/approvals/sql](https://hibira.io/zh-cn/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/) 试试：选一项变更，用手机扫二维码，然后点批准。

## 步骤 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 };
}
```

主管会收到带链接的推送通知（或直接扫发起人页面上的二维码），在以自己语言显示的页面上回复。

![主管手机上的批准页面：SQL、预计影响行数、SHA-256，以及批准和拒绝。](https://hibira.io/portal/articles/sql-approvals-page-zh-CN.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-CN-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-CN.mp4)

## 在你用的工具里做同样的把关

- **Vercel AI SDK、Mastra、LangGraph（JavaScript 和 Python）**：[@hibira-io/approvals](https://hibira.io/zh-cn/guides/approvals) 每一种都有示例：把 `needsApproval` 或 `requireApproval` 写成按 SQL 判断的函数，并在你自己的应用里用审批收件箱显示每个调用的预览。
- **Laravel**：[filament-approvals](https://hibira.io/zh-cn/guides/filament-approvals) 直接在 Filament 后台里审批，README 里有给 Laravel AI SDK 用的 `RunSql` 工具。
- **[n8n](https://hibira.io/zh-cn/guides/n8n)、[Make](https://hibira.io/zh-cn/guides/make)、[Activepieces](https://hibira.io/zh-cn/guides/activepieces) 和 Dify**：用代码步骤判断 SQL 类型，Hibira 的审批步骤请主管决定并等待结果，数据库步骤只接在“批准”的分支上。每个的 README 都有完整流程。
- **Claude、Cursor 和其他 MCP 客户端**：[MCP server](https://hibira.io/zh-cn/mcp-server) 的 `request_approval` 也能在对话中用同样的方式提交审批。
- **任何数据库 MCP server**：[mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) 不用改代码，就能包住 DBHub、postgres-mcp、Supabase MCP 等 server。
