把 Lua 嵌进 psql:用自定义反斜杠命令扩展 PostgreSQL 终端

2026-08-27 33 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

psql 的内置命令稳定而实用,但一旦团队希望调整 \dt 的排序、参数或输出,就会碰到一个现实问题:通用工具很难为每种工作流增加专用语法。Pavel Stehule 准备的一组补丁尝试把 Lua 集成进 psql,让用户通过脚本注册自己的反斜杠命令。例如,可以实现一个按表大小排序的 \my.dt+,而不必修改和重新编译对应命令的 C 代码。

需要明确的是,这里讨论的是补丁所展示的扩展机制。下面的 Lua API 不能假定存在于所有 PostgreSQL 正式版本中;实际测试前,应确认所使用的 psql 已应用相关补丁,并通过 :{?LUA_RELEASE} 检查 Lua 支持。

为什么不直接修改 \dt

十年前,作者曾尝试让 \dt+ 支持按大小排序。看起来只是增加一个 ORDER BY pg_table_size(...),实际却会引出一连串接口问题:新参数应该放在哪里,是否与现有模式匹配语法冲突,默认顺序是否改变,以及脚本是否会因输出变化而失效。

后来出现的 pspg 从展示层解决了问题:用户可以根据光标所在列对查询结果排序。Lua 集成走的是另一条路线,它把扩展点放进 psql 本身,使用户能够:

  • 注册新的反斜杠命令,而不是改变内置命令的兼容行为;
  • 读取普通参数以及命令末尾的选项;
  • 判断命令是否带 +,并据此生成详细输出;
  • 执行 SQL,再交给 psql 原有的结果渲染逻辑;
  • 把团队常用的目录查询固化成可版本管理的脚本。

这类机制的价值不只在于表大小排序。它还适合封装锁等待、复制延迟、膨胀检查和对象权限等日常诊断查询。

自定义命令由哪些部分组成

补丁示例中的核心入口是 psql.registerCommand。命令定义包含名称、帮助文本和处理函数。处理函数接收扫描器、参数缓冲区、命令信息以及 verbose 标记;其中 verbose 对应命令名后的 +

处理过程可以概括为四步:

  1. psql.scanSlashOption 读取模式和排序参数;
  2. schema.table 拆成 schema 与表名;
  3. 根据 + 决定是否查询大小和描述;
  4. 调用 psql.exec 执行 SQL,再用 psql.printQuery 输出结果。

处理函数返回 psql.PSQL_CMD_SKIP_LINE,表示当前反斜杠命令已经消费完毕,psql 不应继续把这一行当作 SQL 处理。

模式过滤尤其需要谨慎。对象名不能直接拼接到 SQL 中。示例 API 暴露了连接对象的 escape 方法,因此至少应对 schema 和关系名转义。若后续 API 支持参数绑定,应优先使用绑定参数;字符串转义仍然要求开发者正确处理引号和模式语义。

可以这样实践:实现 \my.dt

以下示例基于补丁摘要中展示的 API,并假设支持 Lua 的 psql 可以从启动脚本读取 \luacode。将脚本保存为 my-dt.psql,再从 psql 中执行 \i my-dt.psql。如果你的补丁版本调整了 Lua API 名称,需要同步修改注册和扫描调用。

\if :{?LUA_RELEASE}
\echo Lua runtime: :LUA_RELEASE

\luacode
psql.registerCommand({
  name = "my.dt",
  help_syntax = "\\my.dt[+] [SCHEMA.TABLE] [-asc-size|-desc-size]",
  help_desc = "list relations, optionally sorted by table size",

  handler = function(scanner, args, command, verbose)
    local filter = [[
 AND n.nspname <> 'pg_catalog'
 AND n.nspname !~ '^pg_toast'
 AND n.nspname <> 'information_schema'
 AND pg_catalog.pg_table_is_visible(c.oid)
]]
    local sort = " ORDER BY n.nspname, c.relname"
    local opt = psql.scanSlashOption(scanner, psql.OT_NORMAL, false)

    if opt == "-help" then
      print("\\my.dt[+] [SCHEMA.TABLE] [-asc-size|-desc-size]")
      print("  +           include size and description")
      print("  -asc-size   sort by size ascending")
      print("  -desc-size  sort by size descending")
      return psql.PSQL_CMD_SKIP_LINE
    end

    if opt and string.sub(opt, 1, 1) ~= "-" then
      local schema, relation = string.match(opt, "^([^%.]+)%.(.+)$")
      if not relation then
        relation = opt
      end

      if schema == "*" then
        filter = ""
      elseif schema then
        filter = " AND n.nspname = '" ..
          psql.connect():escape(schema) .. "'\n"
      else
        filter = " AND pg_catalog.pg_table_is_visible(c.oid)\n"
      end

      if relation and relation ~= "*" then
        filter = filter .. " AND c.relname = '" ..
          psql.connect():escape(relation) .. "'\n"
      end

      opt = psql.scanSlashOption(scanner, psql.OT_NORMAL, false)
    end

    if opt == "-asc-size" then
      sort = " ORDER BY pg_catalog.pg_table_size(c.oid) ASC"
    elseif opt == "-desc-size" then
      sort = " ORDER BY pg_catalog.pg_table_size(c.oid) DESC"
    elseif opt then
      print("unknown option: " .. opt)
      return psql.PSQL_CMD_SKIP_LINE
    end

    local query = [[
SELECT n.nspname AS "Schema",
       c.relname AS "Name",
       CASE c.relkind
         WHEN 'r' THEN 'table'
         WHEN 'v' THEN 'view'
         WHEN 'm' THEN 'materialized view'
       END AS "Type",
       pg_catalog.pg_get_userbyid(c.relowner) AS "Owner"
]]

    if verbose then
      query = query .. [[,
       pg_catalog.pg_size_pretty(
         pg_catalog.pg_table_size(c.oid)
       ) AS "Size",
       pg_catalog.obj_description(c.oid, 'pg_class') AS "Description"
]]
    end

    query = query .. [[
  FROM pg_catalog.pg_class AS c
  LEFT JOIN pg_catalog.pg_namespace AS n
    ON n.oid = c.relnamespace
 WHERE c.relkind IN ('r', 'v', 'm')
]] .. filter .. sort

    psql.printQuery(psql.exec(query))
    return psql.PSQL_CMD_SKIP_LINE
  end
})
\.

\else
\warn This psql build does not expose Lua support
\endif

加载并调用命令:

\i my-dt.psql
\my.dt
\my.dt+
\my.dt+ pg_catalog.* -desc-size
\my.dt public.orders -asc-size
\my.dt -help

\my.dt+ pg_catalog.* -desc-size 会查询 pg_catalog 中的关系,显示大小和描述,并按 pg_table_size 从大到小排列。没有 + 时,查询不会选择大小与描述列,不过使用大小排序仍然需要数据库计算每个候选对象的大小。

若希望每次启动都加载它,可以在确认脚本可信后,从 ~/.psqlrc 引入:

\i /absolute/path/to/my-dt.psql

扩展能力也扩大了风险边界

Lua 脚本比 SQL 宏更灵活,也意味着更大的审计面。团队落地时应至少检查以下几点:

  • 版本兼容性:补丁中的 API 仍可能变化,脚本应记录对应的 PostgreSQL 分支或提交版本。
  • 启动脚本安全:不要从可被其他用户写入的目录加载 Lua 或 psql 配置文件。
  • SQL 注入:所有用户输入都必须转义;若接口提供参数绑定,应改用参数绑定。
  • 查询成本pg_table_size 对大量对象排序会增加目录查询开销,不应在高频监控循环中无条件执行。
  • 权限边界:Lua 扩展不会绕过 PostgreSQL 权限,但脚本可能连接错误的实例或执行超出预期的维护语句。
  • 输出稳定性:自定义命令适合交互操作;供程序消费时,更稳妥的做法仍是固定 SQL、明确列定义并使用机器可读输出格式。

采用时从个人命令开始

这项设计最适合先解决局部且重复的终端工作流。可以从只读命令起步,例如按大小列出表、查看阻塞链或检查复制槽,再为参数解析、特殊对象名和无权限场景建立测试样例。

当脚本开始被整个团队依赖,就应像维护普通代码一样维护它:进入版本库、固定兼容版本、审查输入处理,并避免让自定义命令与内置命令同名。Lua 集成的关键意义不是把 psql 变成一个完整应用平台,而是让那些难以进入通用 CLI 语法的需求,可以在用户侧形成清晰、可复用的命令。


相关推荐