CloudBeaver 26.2.3 API 使用详解

CloudBeaver 26.2.3 GraphQL API 实测详解:/api/gql 单端点、103 Query/84 Mutation 操作分布与 BadFaithIntrospection 内省保护、authLogin 会话认证、连接全生命周期管理、异步 SQL 执行任务链、导航树元数据与 DDL 获取、CSV 导出下载,以及口令错误与异步并发乱序排障实录。

CloudBeaverCloudBeaverSQLPostgreSQLMariaDB2026/10/25

CloudBeaver 26.2.3 API 使用详解

目标:把 CloudBeaver Community 26.2.3 的 GraphQL API 按「总览 → 认证 → 连接管理 → SQL 执行 → 元数据 → 数据导出」的顺序完整走一遍,所有命令与回显均在本机实测(CloudBeaver Community 26.2.3.202610190842、Ubuntu 24.04.5),取材日期 2026-10-25。部署与账号准备见《CloudBeaver 26.2.3 Docker Compose 部署指南》,会话超时、口令策略等服务端参数见《CloudBeaver 26.2.3 配置参数详解》。

一、环境信息

  • 应用版本:CloudBeaver Community 26.2.3(构建号 26.2.3.202610190842)
  • API 端点:http://127.0.0.1:8978/api/gql(GraphQL over HTTP POST)
  • 演示账号:管理员 root / q1w2e3r4
  • 演示连接:pg-demo(PostgreSQL 16,库 demo)、maria-demo(MariaDB 11.8,库 demo),演示表 sensor_log
  • 调用工具:curl + 一个 10 行的 python3 封装(见 2.3 节,用于避免 SQL 引号在 shell 里的转义问题)

二、API 总览

2.1 单端点与 GraphQL 模型

CloudBeaver 的全部前后端交互都走同一个端点 POST /api/gql,请求体是标准 GraphQL JSON:{"query": "..."}。与常见 GraphQL 服务不同,CloudBeaver 把几乎所有操作都声明为 Query,包括登录 authLogin、建用户 createUser、改密码 setUserCredentials 等「写操作」;真正的数据读写(建连接、执行 SQL)才放在 Mutation 里。因此调用时不能只凭语义猜 query 还是 mutation,写错了会收到 FieldUndefined 校验错误。

2.2 操作分布:103 个 Query 与 84 个 Mutation(实测计数)

通过内省分别统计两类根操作的字段数:

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"{ __schema { queryType { fields { name } } } }"}' -o q.json
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"{ __schema { mutationType { fields { name } } } }"}' -o m.json
python3 -c "import json; print('queryType fields:', len(json.load(open('q.json'))['data']['__schema']['queryType']['fields'])); print('mutationType fields:', len(json.load(open('m.json'))['data']['__schema']['mutationType']['fields']))"

实测回显:

queryType fields: 103
mutationType fields: 84

常用操作速查(按域分组,均实测可调用):

域操作类型
认证authLogin / authLogoutExtended / sessionStateQuery
服务信息serverConfig / sessionPermissionsQuery
用户管理listUsers / createUser / setUserCredentialsQuery
连接userConnections / connectionInfo / driverListQuery
连接createConnection / testConnection / initConnection / closeConnection / deleteConnectionMutation
SQLasyncSqlExecuteQuery / asyncSqlExecuteResults / sqlContextCreate / sqlContextDestroyMutation
任务asyncTaskInfo / asyncTaskCancelMutation
元数据navNodeChildren / navNodeInfo / metadataGetNodeDDLQuery
导出dataTransferExportDataFromContainer / dataTransferAvailableStreamProcessorsQuery

认证方式是服务端会话:authLogin 成功后,响应头 Set-Cookie: cb-session-id=<会话串>; HttpOnly 种下 Cookie,后续所有请求携带该 Cookie 即视为已登录,没有 Bearer Token 机制。因为实例开启了匿名访问(anonymousAccessEnabled: true),不带 Cookie 的请求也会自动获得一个匿名会话——可以调用公共接口,但看不到任何用户连接,调用管理接口会被拒绝(见 8.1 节)。

SQL 里大量单引号在 shell 里转义易错,本文用一个最小封装复用 Cookie 并打印美化后的响应,文件 gql.py 内容如下(实测使用的就是它):

import json, sys, urllib.request, http.cookiejar
cj = http.cookiejar.MozillaCookieJar('ck.txt')
cj.load(ignore_discard=True, ignore_expires=True)
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def gql(query):
    req = urllib.request.Request('http://127.0.0.1:8978/api/gql', data=json.dumps({'query': query}).encode(), headers={'Content-Type': 'application/json'})
    return json.dumps(json.loads(op.open(req).read()), indent=2, ensure_ascii=False)
if __name__ == '__main__':
    print(gql(sys.argv[1]))

用法:python3 gql.py '<GraphQL 查询文本>'。

2.4 内省保护:BadFaithIntrospection

服务带有「恶意内省」拦截:单个请求里 __Type.fields 出现次数过多会被直接拒绝。把 2.2 节的两个统计合并成一次请求即触发:

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"{ __schema { queryType { fields { name } } mutationType { fields { name } } } }"}'

实测回显:

{
  "errors": [
    {
      "message": "This request is not asking for introspection in good faith - __Type.fields is present too often!",
      "extensions": {
        "classification": "BadFaithIntrospection"
      }
    }
  ]
}

实用结论:批量抓取 schema 时要拆成多次小请求,这也是 2.2 节分两次统计的原因。

三、认证与首次调用

3.1 免登录探测 serverConfig

serverConfig 不需要登录即可调用,适合部署后冒烟验证与版本确认:

curl -s -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"query { serverConfig { name version productInfo { name id version buildTime } workspaceId anonymousAccessEnabled supportsCustomConnections enabledAuthProviders defaultNavigatorSettings { showSystemObjects showUtilityObjects } } }"}'

实测回显:

{
  "data": {
    "serverConfig": {
      "name": "CloudBeaver Lab",
      "version": "26.2.3.202610190842",
      "productInfo": {
        "name": "CloudBeaver Community",
        "id": "io.cloudbeaver.product.ce.product",
        "version": "26.2.3.202610190842",
        "buildTime": "October 19, 2026"
      },
      "workspaceId": "DHNATZNLMUVBV",
      "anonymousAccessEnabled": true,
      "supportsCustomConnections": false,
      "enabledAuthProviders": [
        "local"
      ],
      "defaultNavigatorSettings": {
        "showSystemObjects": false,
        "showUtilityObjects": false
      }
    }
  }
}

一次调用同时确认了版本、构建时间、工作区 ID、匿名访问开关与可用认证方式(local 即内置口令认证)。

3.2 authLogin 登录

注意 authLogin 是 Query。用 curl -c ck.txt 把会话 Cookie 落盘,供后续 -b ck.txt 复用:

curl -s -c ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"query { authLogin(provider: \"local\", credentials: { user: \"root\", password: \"q1w2e3r4\" }) { userTokens { userId displayName loginTime } } }"}'

实测回显:

{
  "data": {
    "authLogin": {
      "userTokens": [
        {
          "userId": "root",
          "displayName": "root",
          "loginTime": "2026-10-25T19:25:27.222+08:00"
        }
      ]
    }
  }
}

落盘的 Cookie 文件内容(grep cb-session-id ck.txt 实测回显):

#HttpOnly_127.0.0.1	FALSE	/	FALSE	1792931127	cb-session-id	l0xl9dtgmgzy12afx12noym458

3.3 会话状态查询与登出

sessionState 查看当前会话:

python3 gql.py 'query { sessionState { createTime lastAccessTime cacheExpired valid } }'

实测回显:

{
  "data": {
    "sessionState": {
      "createTime": "2026-10-25T11:25:27Z",
      "lastAccessTime": "2026-10-25T11:25:27Z",
      "cacheExpired": false,
      "valid": true
    }
  }
}

登出操作叫 authLogoutExtended(同样是 Query,不是 authLogout——后者在 schema 里不存在,会直接报 FieldUndefined):

python3 gql.py 'query { authLogoutExtended { redirectLinks { url sameTabRedirect } } }'

实测回显:

{
  "data": {
    "authLogoutExtended": {
      "redirectLinks": []
    }
  }
}

登出后服务端把会话降级为匿名会话:sessionState.valid 仍为 true(匿名会话也是合法会话),但 userConnections 返回空数组,管理接口返回 accessDenied(见 8.1 节)。本文后续演示前已重新执行 3.2 节登录。

四、连接管理域

4.1 列出连接 userConnections

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"query { userConnections { id name driverId host port databaseName connected } }"}'

实测回显(服务重建后连接仍在,说明连接定义持久化在工作区,与会话无关):

{
  "data": {
    "userConnections": [
      {
        "id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
        "name": "pg-demo",
        "driverId": "postgresql:postgres-jdbc",
        "host": "pg",
        "port": "5432",
        "databaseName": "demo",
        "connected": false
      },
      {
        "id": "mariaDB-1a172ab0e54-d5008b42ec7a307",
        "name": "maria-demo",
        "driverId": "mysql:mariaDB",
        "host": "maria",
        "port": "3306",
        "databaseName": "demo",
        "connected": false
      }
    ]
  }
}

连接 id 由「驱动短名-时间戳-哈希」组成,是后续所有连接级操作的入参。connected: false 表示定义存在但尚未建立 JDBC 连接。

4.2 测试连接 testConnection(错误口令排障)

testConnection 用一份临时配置试连,不落库。故意给错误口令:

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"mutation { testConnection(config: { driverId: \"postgresql:postgres-jdbc\", host: \"pg\", port: \"5432\", databaseName: \"demo\", credentials: { userName: \"postgres\", userPassword: \"wrong-pass\" } }) { id connected connectionError { message errorCode } } }"}'

实测回显(错误经 GraphQL errors 数组返回,extensions 里带 Java 异常类、SQLState 与完整堆栈):

{
  "errors": [
    {
      "message": "Error connecting to database:\nConnection failed:\nFATAL: password authentication failed for user \"postgres\"",
      "locations": [
        {
          "line": 1,
          "column": 12
        }
      ],
      "path": [
        "testConnection"
      ],
      "extensions": {
        "exceptionClass": "org.postgresql.util.PSQLException",
        "stackTrace": "org.postgresql.util.PSQLException: FATAL: password authentication failed for user \"postgres\"\n\tat org.postgresql.Driver$ConnectTask.getResult(Driver.java:399)\n\tat org.postgresql.Driver.connect(Driver.java:311)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCConnectionOpener.run(JDBCConnectionOpener.java:112)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCConnectionOpener.run(JDBCConnectionOpener.java:86)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.openConnection(JDBCDataSource.java:262)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.openConnection(PostgreDataSource.java:591)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.openConnection(PostgreDataSource.java:522)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCExecutionContext.connect(JDBCExecutionContext.java:114)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCRemoteInstance.lambda$initializeMainContext$0(JDBCRemoteInstance.java:106)\n\tat org.jkiss.dbeaver.model.exec.DBExecUtils.tryOpenContext(DBExecUtils.java:1176)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCRemoteInstance.initializeMainContext(JDBCRemoteInstance.java:105)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDatabase.checkInstanceConnection(PostgreDatabase.java:236)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDatabase.<init>(PostgreDatabase.java:121)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.createDatabaseImpl(PostgreDataSource.java:264)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.initializeRemoteInstance(PostgreDataSource.java:158)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.<init>(JDBCDataSource.java:109)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.<init>(JDBCDataSource.java:100)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.<init>(PostgreDataSource.java:95)\n\tat org.jkiss.dbeaver.ext.postgresql.PostgreDataSourceProvider.openDataSource(PostgreDataSourceProvider.java:128)\n\tat org.jkiss.dbeaver.ext.postgresql.PostgreDataSourceProvider.openDataSource(PostgreDataSourceProvider.java:45)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.openDataSource(DataSourceDescriptor.java:1409)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.connect0(DataSourceDescriptor.java:1205)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.connect(DataSourceDescriptor.java:1060)\n\tat org.jkiss.dbeaver.runtime.jobs.ConnectJob.run(ConnectJob.java:76)\n\tat org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob.run(ConnectionTestJob.java:104)\n\tat io.cloudbeaver.service.core.impl.WebServiceCore.testConnection(WebServiceCore.java:591)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)",
        "errorCode": 0,
        "databaseState": "28P01",
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": null
}

排障要点:message 三行结构固定为「CloudBeaver 包装信息 → 连接阶段 → 驱动原始报错」;databaseState: "28P01" 即 PostgreSQL 的口令认证失败 SQLState;exceptionClass 直接指出是 JDBC 驱动抛出的异常,说明问题在目标库而非 CloudBeaver 本身。

4.3 建立连接 initConnection

对已保存的连接发起真正的 JDBC 连接。saveCredentials: false 的连接需在此传 credentials:

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"mutation { initConnection(id: \"postgres-jdbc-1a172a8859d-66a0bb96dac402dc\", credentials: { userName: \"postgres\", userPassword: \"q1w2e3r4\" }) { id name connected connectTime serverVersion connectionError { message } } }"}'

实测回显:

{
  "data": {
    "initConnection": {
      "id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
      "name": "pg-demo",
      "connected": true,
      "connectTime": "2026-10-25T11:26:47Z",
      "serverVersion": null,
      "connectionError": null
    }
  }
}

4.4 创建连接 createConnection

createConnection 把连接定义持久化到工作区。驱动 ID 可用 driverList 查询(CE 26.2.3 预下载 18 个驱动,清单见《CloudBeaver 26.2.3 配置参数详解》第十章)。创建一个 MariaDB 连接:

python3 gql.py 'mutation { createConnection(config: { name: "maria-api", driverId: "mysql:mariaDB", host: "maria", port: "3306", databaseName: "demo", credentials: { userName: "root", userPassword: "q1w2e3r4" }, saveCredentials: true }) { id name driverId host port connected } }'

实测回显:

{
  "data": {
    "createConnection": {
      "id": "mariaDB-1a172d5362b-6da56f019a098507",
      "name": "maria-api",
      "driverId": "mysql:mariaDB",
      "host": "maria",
      "port": "3306",
      "connected": false
    }
  }
}

saveCredentials: true 把口令一并保存,之后 initConnection 无需再传凭据:

python3 gql.py 'mutation { initConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") { id name connected connectTime } }'

实测回显:

{
  "data": {
    "initConnection": {
      "id": "mariaDB-1a172d5362b-6da56f019a098507",
      "name": "maria-api",
      "connected": true,
      "connectTime": "2026-10-25T11:32:27Z"
    }
  }
}

顺手在该连接上验证 MariaDB 可用(执行流程见第五章):

{
  "data": {
    "asyncSqlExecuteResults": {
      "statusMessage": "Executed",
      "duration": 8,
      "results": [
        {
          "resultSet": {
            "columns": [
              {
                "name": "ver",
                "typeName": "VARCHAR"
              }
            ],
            "rowsWithMetaData": [
              {
                "data": [
                  "11.8.9-MariaDB-ubu2404"
                ]
              }
            ]
          }
        }
      ]
    }
  }
}

4.5 关闭与删除 closeConnection / deleteConnection

关闭只断开 JDBC 会话,删除才移除工作区里的定义:

python3 gql.py 'mutation { closeConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") { id name connected } }'

实测回显:

{
  "data": {
    "closeConnection": {
      "id": "mariaDB-1a172d5362b-6da56f019a098507",
      "name": "maria-api",
      "connected": false
    }
  }
}
python3 gql.py 'mutation { deleteConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") }'

实测回显:

{
  "data": {
    "deleteConnection": true
  }
}

再次 userConnections 确认只剩 pg-demo 与 maria-demo,maria-api 已被移除。

五、SQL 执行域(异步任务模型)

5.1 创建 SQL 上下文 sqlContextCreate

SQL 执行不是无状态的:先在某个已建立的连接上创建 SQL 上下文(可指定默认 catalog/schema,相当于会话内的当前库):

curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
  -d '{"query":"mutation { sqlContextCreate(connectionId: \"postgres-jdbc-1a172a8859d-66a0bb96dac402dc\", defaultCatalog: \"demo\", defaultSchema: \"public\") { id connectionId defaultCatalog defaultSchema } }"}'

实测回显:

{
  "data": {
    "sqlContextCreate": {
      "id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1",
      "connectionId": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
      "defaultCatalog": "demo",
      "defaultSchema": "public"
    }
  }
}

上下文 id 规则为 <连接id>:<序号>,同连接可开多个互相隔离的上下文;sqlContextDestroy 用于显式销毁。

5.2 提交异步执行 asyncSqlExecuteQuery

所有 SQL 都包装成异步任务,调用立即返回任务 id(会话内自增整数),不阻塞等待结果:

python3 gql.py 'mutation { asyncSqlExecuteQuery(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", contextId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1", sql: "INSERT INTO sensor_log (device, value) VALUES ('"'"'sensor-a'"'"', 21.5), ('"'"'sensor-b'"'"', 19.8), ('"'"'sensor-a'"'"', 22.1)") { id running } }'

实测回显:

{
  "data": {
    "asyncSqlExecuteQuery": {
      "id": "8",
      "running": true
    }
  }
}

5.3 轮询任务 asyncTaskInfo

python3 gql.py 'mutation { asyncTaskInfo(id: "8", removeOnFinish: false) { id running status error { message } } }'

实测回显:

{
  "data": {
    "asyncTaskInfo": {
      "id": "8",
      "running": false,
      "status": "Finished",
      "error": null
    }
  }
}

removeOnFinish: false 表示查询后保留任务结果,供下一步取数;任务失败时 error.message 带 SQLState(见 5.5 节排障实录)。

5.4 取回结果 asyncSqlExecuteResults

任务结束后取执行信息。DML 看 updateRowCount:

python3 gql.py 'mutation { asyncSqlExecuteResults(taskId: "8") { statusMessage duration results { updateRowCount dataFormat sourceQuery } } }'

实测回显(3 行插入,耗时 2ms):

{
  "data": {
    "asyncSqlExecuteResults": {
      "statusMessage": "Executed",
      "duration": 2,
      "results": [
        {
          "updateRowCount": 3.0,
          "dataFormat": "resultset",
          "sourceQuery": null
        }
      ]
    }
  }
}

SELECT 则取 resultSet 里的列元数据与行数据。先提交查询任务(返回 id: "9"),再取结果:

python3 gql.py 'mutation { asyncSqlExecuteQuery(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", contextId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1", sql: "SELECT id, device, value, logged_at FROM sensor_log ORDER BY id") { id running } }'
python3 gql.py 'mutation { asyncSqlExecuteResults(taskId: "9") { statusMessage duration results { updateRowCount dataFormat sourceQuery resultSet { id columns { label name dataKind typeName } rowsWithMetaData { data } } } } }'

实测回显(列类型带 PG 内部类型名,行数据全部按字符串序列化):

{
  "data": {
    "asyncSqlExecuteResults": {
      "statusMessage": "Executed",
      "duration": 46,
      "results": [
        {
          "updateRowCount": null,
          "dataFormat": "resultset",
          "sourceQuery": null,
          "resultSet": {
            "id": "1",
            "columns": [
              {
                "label": "id",
                "name": "id",
                "dataKind": "NUMERIC",
                "typeName": "serial4"
              },
              {
                "label": "device",
                "name": "device",
                "dataKind": "STRING",
                "typeName": "text"
              },
              {
                "label": "value",
                "name": "value",
                "dataKind": "NUMERIC",
                "typeName": "numeric"
              },
              {
                "label": "logged_at",
                "name": "logged_at",
                "dataKind": "DATETIME",
                "typeName": "timestamptz"
              }
            ],
            "rowsWithMetaData": [
              {
                "data": [
                  "1",
                  "sensor-a",
                  "21.5",
                  "2026-10-25 19:29:37.858 +0800"
                ]
              },
              {
                "data": [
                  "2",
                  "sensor-b",
                  "19.8",
                  "2026-10-25 19:29:37.858 +0800"
                ]
              },
              {
                "data": [
                  "3",
                  "sensor-a",
                  "22.1",
                  "2026-10-25 19:29:37.858 +0800"
                ]
              }
            ]
          }
        }
      ]
    }
  }
}

注意:行值统一序列化为字符串(21.5 而非数值),消费方要按 columns[].dataKind 自行转换。

5.5 排障实录:并发提交导致 DDL/DML 乱序

实测中曾把 DROP TABLE IF EXISTS sensor_log、CREATE TABLE sensor_log (...)、INSERT INTO ... 三条语句连续提交(任务 1/2/3),随后 INSERT 任务报错:

{
  "data": {
    "asyncTaskInfo": {
      "id": "3",
      "name": "SQL execute",
      "running": false,
      "status": "Process query INSERT INTO sensor_log (device, value) VALUES ('sensor-a', 21.5), ('sensor-b', 19.8), ('sensor-a', 22.1)",
      "error": {
        "message": "Error executing query:\nSQL Error [42P01]: ERROR: relation \"sensor_log\" does not exist\n  Position: 13"
      },
      "taskResult": null
    }
  }
}

但任务 1、2 都显示 status: "Finished"、error: null。直接在库里核查发现表不存在——三条异步任务并发执行,DROP 实际晚于 CREATE 完成,把刚建的表删掉了。改为「提交一条 → 轮询 asyncTaskInfo 确认 running: false 且无 error → 再提交下一条」后,CREATE 落库成功、INSERT 返回 updateRowCount: 3.0。结论:多语句之间有依赖时必须串行轮询,或合并为一个脚本一次提交,不能依赖提交顺序。

六、元数据与 DDL 域

6.1 导航树下钻 navNodeChildren

数据库对象组织成一棵导航树,节点用 URI 定位,格式为 node://g_GlobalConfiguration/datasources/<连接id>/<层级路径>。从连接根开始逐级下钻(每次取 name nodeType uri hasChildren):

python3 gql.py 'query { navNodeChildren(parentPath: "node://g_GlobalConfiguration/datasources") { name nodeType uri hasChildren folder } }'

实测回显(连接层):

{
  "data": {
    "navNodeChildren": [
      {
        "name": "maria-demo",
        "nodeType": "Connection",
        "uri": "node://g_GlobalConfiguration/datasources/mariaDB-1a172ab0e54-d5008b42ec7a307",
        "hasChildren": true,
        "folder": false
      },
      {
        "name": "pg-demo",
        "nodeType": "Connection",
        "uri": "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
        "hasChildren": true,
        "folder": false
      }
    ]
  }
}

逐级下钻 pg-demo 依次得到 database(Databases)→ database/demo_16384(库节点,URI 带 PG OID 后缀)→ .../schema → schema/public_2200 → .../table,最终列出表节点:

{
  "data": {
    "navNodeChildren": [
      {
        "name": "sensor_log",
        "nodeType": "Table",
        "uri": "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400",
        "hasChildren": true
      }
    ]
  }
}

URI 必须由上级查询返回,不能凭名字拼接——对象节点带 OID 后缀(demo_16384、public_2200、sensor_log_16400),凭猜测拼出的 URI 会报 Node '...' not found(io.cloudbeaver.DBWebException)。

6.2 获取对象 DDL metadataGetNodeDDL

拿上一步的表节点 URI 取 DDL(参数名是 nodeId,不是 nodePath):

python3 gql.py 'query { metadataGetNodeDDL(nodeId: "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400") }'

实测回显:

{
  "data": {
    "metadataGetNodeDDL": "-- Drop table\n\n-- DROP TABLE public.sensor_log;\n\nCREATE TABLE public.sensor_log (\n\tid serial4 NOT NULL,\n\tdevice text NOT NULL,\n\tvalue numeric(8, 2) NULL,\n\tlogged_at timestamptz DEFAULT now() NULL,\n\tCONSTRAINT sensor_log_pkey PRIMARY KEY (id)\n);\n"
  }
}

七、数据导出域

7.1 可用导出格式

python3 gql.py 'query { dataTransferAvailableStreamProcessors { id name fileExtension } }'

实测回显:

{
  "data": {
    "dataTransferAvailableStreamProcessors": [
      {
        "id": "stream_consumer:stream.csv",
        "name": "CSV",
        "fileExtension": "csv"
      },
      {
        "id": "stream_consumer:stream.dbunit",
        "name": "DbUnit",
        "fileExtension": "xml"
      },
      {
        "id": "stream_consumer:stream.html",
        "name": "HTML",
        "fileExtension": "html"
      },
      {
        "id": "stream_consumer:stream.json",
        "name": "JSON",
        "fileExtension": "json"
      },
      {
        "id": "stream_consumer:stream.markdown.table",
        "name": "Markdown",
        "fileExtension": "md"
      },
      {
        "id": "stream_consumer:stream.sql",
        "name": "SQL",
        "fileExtension": "sql"
      },
      {
        "id": "stream_consumer:stream.source.code",
        "name": "Source code",
        "fileExtension": "php"
      },
      {
        "id": "stream_consumer:stream.txt",
        "name": "TXT",
        "fileExtension": "txt"
      },
      {
        "id": "stream_consumer:stream.xml",
        "name": "XML",
        "fileExtension": "xml"
      },
      {
        "id": "stream_consumer:stream.xlsx",
        "name": "XLSX",
        "fileExtension": "xlsx"
      }
    ]
  }
}

7.2 提交导出任务

dataTransferExportDataFromContainer 直接对表节点导出(processorProperties 是必填,给空对象即用默认 CSV 参数):

python3 gql.py 'query { dataTransferExportDataFromContainer(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", containerNodePath: "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400", parameters: { processorId: "stream_consumer:stream.csv", processorProperties: {} }) { id running status } }'

实测回显(小表同步就完成了):

{
  "data": {
    "dataTransferExportDataFromContainer": {
      "id": "10",
      "running": false,
      "status": "Finished"
    }
  }
}

从任务结果里取文件名:

python3 gql.py 'mutation { asyncTaskInfo(id: "10", removeOnFinish: false) { id running status taskResult error { message } } }'

实测回显:

{
  "data": {
    "asyncTaskInfo": {
      "id": "10",
      "running": false,
      "status": "Finished",
      "taskResult": "l0xl9dtgmgzy12afx12noym458_424a49e4-239c-4452-8d44-e41bde5460a1.csv",
      "error": null
    }
  }
}

7.3 下载导出文件

导出文件经普通 HTTP GET 下载,路径为 /api/data/<taskResult 文件名>,需带同一会话 Cookie:

curl -s -b ck.txt -D - http://127.0.0.1:8978/api/data/l0xl9dtgmgzy12afx12noym458_424a49e4-239c-4452-8d44-e41bde5460a1.csv -o sensor_log.csv

实测响应头与文件内容:

HTTP/1.1 200 OK
Date: Sun, 25 Oct 2026 11:31:40 GMT
Content-Type: text/plain;charset=iso-8859-1
Content-Disposition: attachment; filename="sensor_log.csv"
Transfer-Encoding: chunked
"id","device","value","logged_at"
1,sensor-a,21.50,2026-10-25 19:29:37.858 +0800
2,sensor-b,19.80,2026-10-25 19:29:37.858 +0800
3,sensor-a,22.10,2026-10-25 19:29:37.858 +0800

八、错误格式与排障速查

8.1 三类错误形态

实测遇到的错误统一经 GraphQL errors 数组返回,按 extensions.classification 分三类:

分类触发场景实测特征
ValidationError字段名/参数写错(如把管理操作当 mutation、字段名拼错)message 以 Validation error (FieldUndefined...) 开头,HTTP 200
DataFetchingException业务/数据库错误(口令错误、SQL 报错、节点不存在)extensions 带 exceptionClass(如 PSQLException)、databaseState(SQLState)、完整 Java 堆栈
BadFaithIntrospection单请求内省字段过多见 2.4 节

注意:业务错误时 HTTP 状态码仍是 200,必须检查响应体里的 errors,不能只判断传输层状态。

8.2 权限与会话错误码

webErrorCode 扩展字段用于程序化处理。未登录调用管理接口 listUsers 实测:

{
  "errors": [
    {
      "message": "Access denied",
      "locations": [
        {
          "line": 1,
          "column": 9
        }
      ],
      "path": [
        "listUsers"
      ],
      "extensions": {
        "exceptionClass": "io.cloudbeaver.DBWebExceptionAccessDenied",
        "stackTrace": "io.cloudbeaver.DBWebExceptionAccessDenied: Access denied",
        "webErrorCode": "accessDenied",
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": null
}

会话过期(cb-session-id 失效后)再调用的实测:

{
  "errors": [
    {
      "message": "Session has expired",
      "locations": [
        {
          "line": 1,
          "column": 9
        }
      ],
      "path": [
        "sessionState"
      ],
      "extensions": {
        "exceptionClass": "io.cloudbeaver.DBWebException",
        "stackTrace": "io.cloudbeaver.DBWebException: Session has expired\n\tat io.cloudbeaver.service.session.CBSessionManager.findWebSession(CBSessionManager.java:344)",
        "webErrorCode": "sessionExpired",
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": null
}

消费方收到 sessionExpired 应重新 authLogin;会话空闲超时由服务端 expireSessionAfterPeriod 控制(默认 1800000ms,改法见《CloudBeaver 26.2.3 配置参数详解》)。

8.3 排障速查表

现象原因处理
Field 'authLogout' in type 'Mutation' is undefined把 Query 当 Mutation 用(CloudBeaver 大量操作是 Query,且登出叫 authLogoutExtended)换用 query { ... },或先内省确认
BadFaithIntrospection单次内省请求字段过多拆成多次小请求
Node '...' not found导航 URI 凭名字拼接逐级 navNodeChildren 取真实 URI(带 OID 后缀)
异步任务 error 带 SQL Error [42P01]多条 SQL 并发提交导致乱序串行提交并轮询 asyncTaskInfo,见 5.5 节
asyncSqlExecuteResults 返回 null 值错误任务结果已被取走或任务失败无结果集先查 asyncTaskInfo 确认成功;结果只取一次
Missing field argument 'processorProperties'导出参数缺必填子字段传 processorProperties: {} 用默认

九、实战封装建议

把 2.3 节的 gql.py 扩成三行一调的执行器即可覆盖 90% 场景:exec_sql(sql) 内部按「asyncSqlExecuteQuery 提交 → 循环 asyncTaskInfo 至 running: false → asyncSqlExecuteResults 取回」串行执行,正好规避 5.5 节的并发乱序问题。集成到运维脚本时注意三点:行值全是字符串需按 dataKind 转型;errors 数组必须先于 data 检查;长会话脚本要在捕获 sessionExpired 后自动重登。

十、参考链接