
Bytebase DiffMetadata 重构解析从 SQLService 匿名调用到 DatabaseService 的 IAM 门控 DDL 生成【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读本文基于 Bytebase 仓库中已批准的架构设计文档2026-07-29-diff-metadata-database-service-design.md完整剖析DiffMetadata接口从SQLService迁往DatabaseService并重塑请求契约的工程决策服务端改为按数据库资源名从存储读取源当前schema新增bb.databases.diffMetadata权限并把该能力收紧到 schema 变更编写角色。读完本文你将掌握该 RPC 的 Proto 契约、IAM 授权边界、后端实现调用链、前端调用改造以及 3.21 版本兼容性影响并能直接定位到仓库中的对应源码与测试。背景一次从无凭证纯函数到受管资源读取的接口重塑Bytebase 原有的SQLService.DiffMetadata是一个无凭证anonymous纯函数调用方把两份完整 schema源与目标同时上传给服务端服务端不触碰任何存储数据直接对两份元数据求差并生成迁移 DDL因此被标记为allow_without_credential true。这个形态存在两个结构性缺陷数据新鲜度与语义错位源 schema 由调用方自备可能与数据库中真实同步的 schema 脱节安全级别错配既然不读取存储内容接口无需鉴权但它生成的是变更 DDL属于变更编写change authoring行为理应由 IAM 管控。因此设计文档2026-07-29 批准决定做一次彻底的迁移与重塑迁移DiffMetadata从SQLService移到DatabaseService与已有的DiffSchema并列重塑请求只携带数据库资源名name与目标DatabaseMetadata源 schema 与引擎由服务端从存储读取旧请求中的engine字段随之消失加锁新 RPC 读取存储中的 schema 内容安全类别升级为 IAM 门控引入新权限bb.databases.diffMetadata删旧旧SQLService.DiffMetadata在同一版本3.21中直接删除不做废弃别名兼容clean cut作为带标签的 breaking change 发布。一、Proto 契约请求瘦身、响应不变1.1 新 RPC 与 REST 绑定在 proto/v1/v1/database_service.proto 中DatabaseService紧挨着DiffSchema声明了新的 RPC// Generates migration statements from the databases current schema to the // given target metadata. // Permissions required: bb.databases.diffMetadata rpc DiffMetadata(DiffMetadataRequest) returns (DiffMetadataResponse) { option (google.api.http) { post: /v1/{nameinstances/*/databases/*}:diffMetadata body: * additional_bindings: { post: /v1/{nameprojects/*/instances/*/databases/*}:diffMetadata body: * } }; option (bytebase.v1.permission) bb.databases.diffMetadata; option (bytebase.v1.auth_method) IAM; option (bytebase.v1.mcp_method_class) WRITE; }要点REST 绑定为POST /v1/{nameinstances/*/databases/*}:diffMetadata并保留projects/*/instances/*/databases/*形式作为additional_bindings与DiffSchema的绑定风格镜像一致auth_method IAMACL 拦截器可直接把资源名解析到其所属项目完成鉴权无需任何自定义鉴权代码mcp_method_class WRITE该方法在 MCP 网关中被归入写操作类别与它的生成变更 DDL语义一致。1.2 请求与响应消息请求消息定义在 database_service.protomessage DiffMetadataRequest { // The database whose current schema is the diff source. // Format: instances/{instance}/databases/{database} or projects/{project}/instances/{instance}/databases/{database} string name 1 [ (google.api.field_behavior) REQUIRED, (google.api.resource_reference) {type: bytebase.com/Database} ]; // The metadata of the target schema. The source metadata and the engine are // read from the database, so only the target travels in the request. // Must describe the COMPLETE target schema: the diff runs against the full // stored source, so any object omitted from the target (for example by a // truncated metadata fetch) is treated as dropped. DatabaseMetadata target_metadata 2 [(google.api.field_behavior) REQUIRED]; } message DiffMetadataResponse { // The generated migration statements. string diff 1; }值得注意 proto 注释中明确写出的完整性completeness约束diff 是针对完整的存储源 schema 进行的目标中任何被省略的对象例如被截断的元数据获取导致遗漏的表都会被解读为删除。这是本设计埋下的一个关键陷阱后文前端改造一节会展开说明它曾如何在代码评审中被发现。响应仍只有一个string diff字段承载生成的迁移语句。1.3 SQLService 的瘦身sql_service.proto 中删除了该 RPC 及其两个消息并移除import v1/database_service.proto。由于DatabaseMetadata是 SQL 执行服务唯一用到的 database-service 类型删除后SQL 执行服务与数据库类型彻底解耦——这是这次重塑带来的一个附带收益。仓库佐证生成代码 backend/generated-go/v1/database_service.pb.go 与 Connect 桩 backend/generated-go/v1/v1connect/database_service.connect.go 中均已存在DiffMetadata而 grpc-doc 生成文档 proto/gen/grpc-doc/v1/README.md 中已无SQLService/DiffMetadata条目说明旧接口确已移除。二、IAM 权限与角色边界变更编写 ≠ schema 读取2.1 新权限的定义与同步权限常量定义在 backend/common/permission/permission.go#L32DatabasesDiffMetadata Permission bb.databases.diffMetadata声明式清单同步在 backend/common/permission/permission.yaml#L19。前端侧的 TypeScript 生成类型通过 frontend/scripts/copy_config_files.sh 同步生成。2.2 预置角色的授权范围精确到四个变更编写角色在 backend/store/predefined_roles.go 中permission.DatabasesDiffMetadata恰好出现在四个预置角色中分别对应 workspaceAdmin、workspaceDBA、projectOwner、projectDeveloper角色是否拥有bb.databases.diffMetadataWorkspace admin✅Workspace DBA✅Project owner✅Project developer✅Project viewer❌Project releaser❌SQL editor 两个角色❌这一取舍背后是明确的语义决策2026-07-29生成迁移 DDL 属于变更编写行为而非 schema 读取行为。即使 diff 输出本身不会泄露超出bb.databases.getSchema能获取的信息权限模型依然刻意收紧——per-method 权限让角色设计保持显式DDL 生成只归属 authoring 角色而非 read 角色。自定义角色需要显式添加该权限这一点写入了发布说明release notes。三、后端实现走查五步调用链新实现位于 backend/api/v1/database_service.go紧邻DiffSchema。核心流程可拆解为五步func (s *DatabaseService) DiffMetadata(ctx context.Context, req *connect.Request[v1pb.DiffMetadataRequest]) (*connect.Response[v1pb.DiffMetadataResponse], error) { request : req.Msg if request.TargetMetadata nil { return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf(target_metadata is required)) } projectID, instanceID, databaseID, err : common.GetDatabaseResourceName(request.Name) if err ! nil { return nil, connect.NewError(connect.CodeInvalidArgument, err) } _, instance, err : s.findDatabaseForResource(ctx, projectID, instanceID, databaseID, false) if err ! nil { return nil, connect.NewError(connect.CodeInvalidArgument, err) } if instance nil { return nil, connect.NewError(connect.CodeNotFound, errors.Errorf(instance %q not found, instanceID)) } engine : instance.Metadata.GetEngine() switch engine { case storepb.Engine_MYSQL, storepb.Engine_POSTGRES, storepb.Engine_TIDB, storepb.Engine_ORACLE, storepb.Engine_MSSQL: default: return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf(unsupported engine: %v, engine)) } sourceDBSchema, err : s.store.GetDBSchema(ctx, store.FindDBSchemaMessage{ Workspace: common.GetWorkspaceIDFromContext(ctx), InstanceID: instanceID, DatabaseName: databaseID, }) if err ! nil { return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, failed to get database schema)) } if sourceDBSchema nil { return nil, connect.NewError(connect.CodeNotFound, errors.Errorf(schema not found for database %q; sync the database first, request.Name)) } storeTargetMetadata : convertV1DatabaseMetadata(request.TargetMetadata) targetDBSchema : model.NewDatabaseMetadata(storeTargetMetadata, nil, nil, engine, store.IsObjectCaseSensitive(instance)) migrationSQL, err : schema.DiffMigration(engine, sourceDBSchema, targetDBSchema) if err ! nil { return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, failed to compute diff between source and target schemas)) } return connect.NewResponse(v1pb.DiffMetadataResponse{ Diff: migrationSQL, }), nil }逐一对应设计文档的五步解析资源名common.GetDatabaseResourceName把name解析为 project/instance/database 三段findDatabaseForResource加载实例workspace 作用域引擎门控沿用旧 RPC 接受的引擎集合——MYSQL、POSTGRES、TIDB、ORACLE、MSSQL其余引擎返回InvalidArgument读取源 schemastore.GetDBSchema直接返回model.DatabaseMetadata无需任何转换若数据库从未同步过 schema返回NotFound提示 sync the database first转换目标convertV1DatabaseMetadata把请求中的 v1 目标元数据转成 store 模型再经model.NewDatabaseMetadata(..., store.IsObjectCaseSensitive(instance))包装。这里有一个值得注意的行为修正旧 handler 硬编码大小写敏感性为true新实现改用实例的实际排序规则行为与DiffSchema保持一致——这对大小写敏感方言如 PostgreSQL 双引号标识符的正确 diff 至关重要生成 diffschema.DiffMigration(engine, source, target)返回迁移语句原样放进DiffMetadataResponse.diff。同时SQLService.DiffMetadata及其实现被删除sql_service.go随之移除plugin/schema依赖。四、前端调用改造签名不变wire 调用切换4.1 generateDiffDDL 的改造前端唯一调用点是 schema 编辑器的 generateDiffDDL.ts被 SchemaEditorSheet.tsx 使用。该工具函数签名保持不变database source target本地短路与校验逻辑继续留在客户端isEqual(source, target)相等时直接返回空语句无网络请求validateDatabaseMetadata(targetMetadata)校验失败时返回 Invalid schema 与校验信息只有 wire 调用从旧的 SQLService 形态切换为const newRequest create(DiffMetadataRequestSchema, { name: database.name, targetMetadata: targetMetadata, }); const diffResponse await databaseServiceClientConnect.diffMetadata(newRequest, { contextValues: createContextValues().set(silentContextKey, true), });从源码结构看generateDiffDDL持有的Database对象原本就会携带 store 拉取的原始元数据因此服务端读取源 schema 在语义上等价且更新鲜fresher。4.2 一个被代码评审拦下的坑limit: 200 截断设计文档明确记录了一次由请求重塑暴露的回归风险SchemaEditorSheet此前用limit: 200拉取基线元数据窗口式编辑器的性能保护PR #17514。旧接口两侧都截断时是安全的但新接口的服务端源是完整schema一旦目标侧省略了任何表diff 就会对每一张被截断的表生成DROP语句。这个问题在评审中被捕获PR #21068修复方案是让 sheet 改为拉取不限量的元数据——这与其他元数据消费者默认即无限制保持一致同时请求的 proto 注释中显式记录了完整性要求。对应的前端 e2e 测试见 schema-editor-diff-insert.spec.ts。五、兼容性影响矩阵3.21表面影响gRPC/Connectbytebase.v1.SQLService/DiffMetadata已删除——破坏性变更。调用方必须切换到bytebase.v1.DatabaseService/DiffMetadata并采用新请求形态RESTPOST /v1/schemaDesign:diffMetadata已删除——破坏性变更。由POST /v1/{nameinstances/*/databases/*}:diffMetadata需鉴权取代匿名 schema diff 访问按设计消失——新 RPC 读取存储 schema要求bb.databases.diffMetadata滚动升级期间缓存的 3.21 前前端 bundleschema 编辑器 DDL 预览会失败刷新后恢复——随 clean cut 一并接受自定义角色需自行添加bb.databases.diffMetadata才能使用新 RPC四个预置角色随版本更新版本发布以--label breaking标记并配有## Breaking Changes章节覆盖方法移除、REST 路径变更与新权限三项。关于clean cut的取舍设计文档强调旧 RPC 是匿名的因此无法通过认证日志排除未知的外部调用方。之所以仍选择直接删除而非保留废弃别名是因为唯一已知调用方是 Bytebase 自己的 schema 编辑器且请求形态已经改变别名只会保留一个死契约dead contract。匿名或外部调用方将收到 unimplemented/404必须改用新 RPC。六、测试与质量保障e2e 钉死角色边界6.1 新增 e2eTestDiffMetadatabackend/tests/diff_metadata_test.go194 行用真实 PostgreSQL 容器验证了完整行为矩阵no-op 目标产生空 diff把拉取到的当前元数据原样回传diff 必须为空——这钉死了 store→v1→store 往返保真度任何转换器丢字段都会在这里暴露为虚假 DDL新增一张表t_diffid integer NOT NULLdiff 包含CREATE TABLE与t_diff且不包含DROP/ALTER单表新增不拖带虚假变更缺 target→InvalidArgument无项目角色的 workspace 成员→PermissionDeniedproject viewer→仍然PermissionDenied钉死角色边界能读 schema 不代表能写 DDLproject developer→ 成功且 diff 与 owner 完全一致。6.2 转换器单元测试TestDiffMetadataPreservesSRIDInvisiblebatch 转换器测试 database_converter_test.go#L85-L129 中的TestDiffMetadataPreservesSRIDInvisible直接练习 handler 实际运行的转换 diff 管线convertV1DatabaseMetadata→DiffMigration一个携带SRID 4326的几何列与一个INVISIBLE 列在源/目标间保持不变仅另一列注释被编辑时diff 不得产生虚假的SRID/INVISIBLE变更也不得出现幻影 DDL——这验证了 v1→store 转换不会剥落空间参考系与列可见性信息。6.3 标准门禁设计文档列出的标准门禁为buf、golangci-lint、go build、e2e、pnpm 套件。七、备选方案权衡为什么最终选择 clean cut设计文档记录了三个被评估后否决的替代方案理解它们有助于把握本设计的边界保留旧的双元数据形态的废弃匿名别名最初确实以Legacy*消息重命名实现过随后被刻意删除——别名只会承载一个唯一已知调用方是自己前端的死契约而匿名表面恰恰是本变更要退役的东西因此选择 clean cutDanny2026-07-29并入DiffSchema作为target_metadataoneof 成员这是诱人的终态每个数据库一个 diff 表面但契约变更幅度超出本次需求且DiffSchema自身仍带着TODO(d): secure it目前还挂在bb.databases.get上见 database_service.proto#L188-L190留待该 TODO 处理时再合并复用bb.databases.getSchema或把新权限授予所有 getSchema 角色被否决——per-method 权限保持角色设计显式DDL 生成属于 authoring 角色而非 read 角色。总结DiffMetadata的重构是 Bytebase 在变更治理主线上的一次典型演进把本应受管的能力从匿名的纯函数收拢进 IAM 门控的资源型 RPC同时通过请求重塑服务端读源、客户端只传目标简化调用契约、修正大小写敏感性处理并以 e2e 测试将项目查看者不可生成变更 DDL这一角色边界永久钉死。对集成方而言最重要的迁移动作是改用DatabaseService.DiffMetadata、补齐bb.databases.diffMetadata权限、并保证传入的目标元数据完整无截断。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考