Bytebase 合并计划检查(Consolidated Plan Check Runs):从行爆炸到单记录模型的设计与实现
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
导读
本文基于 Bytebase 仓库中的两份核心文档——docs/plans/2025-12-23-consolidated-plan-check-runs-design.md(设计)与docs/plans/2025-12-23-consolidated-plan-check-runs-impl.md(实现),系统讲解 Bytebase 将 plan check run(计划检查)从"每个计划 N×types 条记录"重构为"每个计划一条记录"的完整过程。你将掌握:行爆炸问题的成因、proto 数据模型如何重构、CombinedExecutor组合执行器与调度器的简化方式、CI 采样如何前移到配置生成阶段、以及 SQL 数据迁移如何无损合并历史记录。文中所有结论均可在仓库源码(proto/store/store/plan_check_run.proto、backend/store/plan_check_run.go、backend/runner/plancheck/、backend/migrator/migration/3.14/0006##consolidate_plan_check_runs.sql)中得到验证。
一、问题背景:plan check run 的行爆炸
在合并之前,Bytebase 的 plan check run 表按spec × database × check type三个维度为每个 plan 生成记录。设想一个生产环境的典型场景:
- 一个 plan 含 1 个变更数据库 spec;
- 目标覆盖 100 个数据库;
- 每个数据库默认执行 2 种检查(语句建议
STATEMENT_ADVISE、语句摘要报告STATEMENT_SUMMARY_REPORT),开启 gh-ost 时再加 1 种(GHOST_SYNC)。
结果就是100 × 2~3 = 每计划 200~300 行。随着数据库规模增长,plan_check_run表迅速膨胀,直接影响 PostgreSQL 的存储开销与查询性能。此前项目靠CI sampling size(CI 采样)在事后截断记录,本质上只是给行爆炸打补丁(design 文档原文称之为 "post-hoc band-aid"),并没有解决根因。
1.1 设计文档给出的问题陈述
设计文档(docs/plans/2025-12-23-consolidated-plan-check-runs-design.md)开篇即列出三点:
| 问题 | 说明 |
|---|---|
| 记录数量 | #specs × #databases × #types条记录/plan,100 库 × 2~3 类型 = 200~300 行 |
| 性能影响 | 造成 DB 行爆炸,拖累性能与存储 |
| 既有缓解 | CI 采样是事后补救,无法根治 |
二、解决方案与关键决策
设计的核心思路是:Consolidate to one record per plan——每个 plan 只保留一条plan_check_run记录,把该 plan 涉及的所有 target 与检查类型的配置、结果都放进这条记录的 JSONB 字段里。
设计文档的关键决策表如下:
| 决策点 | 选择 |
|---|---|
| 每计划记录数 | 一条(合并所有类型) |
| 执行模型 | 顺序执行、单一 executor(后续可按需优化为并行) |
| 采样时机 | 在配置生成阶段前移应用(sampling applied at config generation time),而非事后 |
| 结果结构 | 扁平数组 + 元数据标签(instance_id / database_name / check_type) |
| 状态模型 | 记录级状态管执行生命周期;每条结果自带状态管检查结论 |
| 迁移方式 | 纯 SQL 聚合迁移 |
这套设计带来的直接收益:plan_check_run表从"每 plan 数百行"收敛为"每 plan 一行",配合(project, plan_id)唯一索引,从数据模型层面杜绝了行爆炸;同时执行单元从"按类型分发"变成"一个 executor 顺序处理所有 target 与类型",调度逻辑大幅简化。
三、数据模型重构:proto 层
3.1 PlanCheckType 枚举
实现文档 Task 1 要求先定义统一的检查类型枚举,替换原先散落在各处的类型常量。当前仓库 proto/store/store/plan_check_run.proto 中的落地形态为:
enum PlanCheckType { PLAN_CHECK_TYPE_UNSPECIFIED = 0; PLAN_CHECK_TYPE_STATEMENT_ADVISE = 1; PLAN_CHECK_TYPE_STATEMENT_SUMMARY_REPORT = 2; PLAN_CHECK_TYPE_GHOST_SYNC = 3; }三种类型分别对应:SQL 语句规范建议(advisor 检查)、语句摘要报告(变更影响面总结)、gh-ost 在线表结构变更同步检查。
3.2 Result 消息的标签字段
合并后的结果需要知道"这条结果属于哪个数据库、哪种检查",因此在PlanCheckRunResult.Result消息中新增目标标识字段。实现文档给出的草案字段为target(资源名格式instances/{instance}/databases/{database})与type;实际仓库中(plan_check_run.proto)还额外增加了sheet_sha256,用于回溯该结果对应的 SQL sheet 内容哈希:
message Result { Advice.Status status = 1; string title = 2; string content = 3; int32 code = 4; // Target identification for consolidated results // Format: instances/{instance}/databases/{database} string target = 7; PlanCheckType type = 8; // sheet_sha256 is the content hash of the SQL sheet used to produce this result. // Empty for checks that are not tied to a SQL sheet. string sheet_sha256 = 9; oneof report { SqlSummaryReport sql_summary_report = 5; SqlReviewReport sql_review_report = 6; } // ... }注意一个实现细节:设计文档初稿使用的是instance_id/database_name两个独立字段,而实际落地改为单个target资源名字符串 +type枚举,这是实现阶段的字段演进,最终形态以仓库为准。
3.3 生成 proto
修改 proto 后需要重新生成 Go 代码:
cd proto && buf generate随后可验证生成产物是否更新:
ls -la backend/generated-go/store/plan_check_run.pb.go当前仓库的 backend/generated-go/store/ 目录即由buf generate产出。
四、存储层重构:Store 层
4.1 消息结构简化
实现文档 Task 2 要求从PlanCheckRunMessage中移除Type字段与类型常量。当前仓库 backend/store/plan_check_run.go 的实际形态更进一步——不仅移除了Type,Config字段也被移除,原因见第六节"配置改为运行时推导":
// PlanCheckRunMessage is the message for a plan check run. type PlanCheckRunMessage struct { UID int64 CreatedAt time.Time UpdatedAt time.Time ProjectID string PlanUID int64 Status PlanCheckRunStatus Result *storepb.PlanCheckRunResult // Generation is the PostgreSQL row-version token. ... Generation int64 }记录级状态枚举也保留为五个值(plan_check_run.go):AVAILABLE、RUNNING、DONE、FAILED、CANCELED。
4.2 单记录访问助手 GetPlanCheckRun
原先按类型查多条记录的ListPlanCheckRuns继续保留(供查询与测试使用),同时新增单记录便捷访问器:
// GetPlanCheckRun returns the plan check run for a plan. func (s *Store) GetPlanCheckRun(ctx context.Context, projectID string, planUID int64) (*PlanCheckRunMessage, error) { runs, err := s.ListPlanCheckRuns(ctx, &FindPlanCheckRunMessage{ProjectID: projectID, PlanUID: &planUID}) if err != nil { return nil, err } if len(runs) == 0 { return nil, nil } return runs[0], nil }该函数在backend/store/plan_check_run.go中落地,并被backend/runner/approval/runner.go、backend/runner/taskrun/下的审批与自动发布消费者大量引用(仓库中backend/tests/、backend/store/的测试也广泛使用)。
4.3 写入与查询的简化
CreatePlanCheckRun(由设计文档中的CreatePlanCheckRuns复数形式演化为单数)通过ON CONFLICT (project, plan_id) DO UPDATE实现"每 plan 一行"的幂等 upsert(plan_check_run.go),并且:
- 先通过
AcquirePlanIssueRolloutAdvisoryLock加锁,保证并发安全; - 校验
plan.config->>'approvalInputVersion'与结果中的版本一致,防止旧版本结果覆盖新版本; - 返回
rowsAffected > 0表示本次确实更新了行。
查询侧ListPlanCheckRuns的扫描逻辑同样去掉了 type 维度,并在FindPlanCheckRunMessage中支持ProjectIDs、PlanUIDs、ResultStatus(通过jsonb_array_elements探测结果数组中是否存在某状态)等过滤条件(plan_check_run.go)。
4.4 行级并发控制(实现演进亮点)
合并模型上线后,"一 plan 一行"使得并发更新冲突面更小,但 Bytebase 又引入了approvalInputVersion(审批输入版本)机制做行级防护:
UpdatePlanCheckRunIfApprovalInputVersion:仅当行仍处于 RUNNING 且版本匹配时才写入 DONE/FAILED 结果;RefreshPlanCheckRunIfStaleApprovalInputVersion:把过期版本的中止态行刷回 AVAILABLE 以便重跑;CancelPlanCheckRunIfApprovalInputVersion:带版本校验的取消;FailStalePlanCheckRuns:对超时 RUNNING 行做兜底 FAILED;ClaimAvailablePlanCheckRuns:用FOR UPDATE SKIP LOCKED原子抢占 AVAILABLE 行,支持多副本调度器并发领单。
这些方法都定义在 backend/store/plan_check_run.go 中,可视为合并重构后为保持数据一致性而补齐的配套机制。
五、组合执行器:CombinedExecutor
5.1 执行器职责
实现文档 Task 3 创建backend/runner/plancheck/executor_combined.go,用单一执行器处理所有检查类型。仓库中的落地实现(executor_combined.go)以RunForTarget为核心入口,针对单个 target 顺序执行其声明的全部检查类型:
func (e *CombinedExecutor) RunForTarget(ctx context.Context, target *CheckTarget) ([]*storepb.PlanCheckRunResult_Result, error) { var allResults []*storepb.PlanCheckRunResult_Result for _, checkType := range target.Types { results, err := e.runCheck(ctx, target, checkType) if err != nil { // Add error result for this target/type, continue to next allResults = append(allResults, &storepb.PlanCheckRunResult_Result{ Status: storepb.Advice_ERROR, Target: target.Target, Type: checkType, SheetSha256: target.SheetSha256, Title: "Check failed", Content: err.Error(), Code: common.Internal.Int32(), }) continue } // Tag results with target info for _, r := range results { r.Target = target.Target r.Type = checkType r.SheetSha256 = target.SheetSha256 } allResults = append(allResults, results...) } return allResults, nil }核心语义与设计文档一致:某个 target/type 的检查失败不会中断整体,而是生成一条Advice_ERROR的结果继续执行下一个检查,保证"部分失败、结果尽量完整"。
5.2 类型分发
runCheck通过 switch 按PlanCheckType分发给既有 executor:
func (e *CombinedExecutor) runCheck(ctx context.Context, target *CheckTarget, checkType storepb.PlanCheckType) ([]*storepb.PlanCheckRunResult_Result, error) { switch checkType { case storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_ADVISE: return e.runStatementAdvise(ctx, target) case storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_SUMMARY_REPORT: return e.runStatementReport(ctx, target) case storepb.PlanCheckType_PLAN_CHECK_TYPE_GHOST_SYNC: return e.runGhostSync(ctx, target) default: return nil, nil } }三个内部方法各自实例化既有的StatementAdviseExecutor、StatementReportExecutor、GhostSyncExecutor并调用其RunForTarget——这些 executor 分别位于 statement_advise_executor.go、statement_report_executor.go、ghost_sync_executor.go。组合执行器因此是复用而非重写:既有检查逻辑零改动,只是被统一编排。
六、配置生成:从存储到运行时推导
6.1 关键演进:CheckTarget 不落库
设计文档原方案是把合并后的PlanCheckRunConfig{targets: [...]}随记录写入configJSONB 列。但当前仓库的 check_target.go 明确注释:target 是运行时从 plan 的 specs 推导出来的,不存储:
// CheckTarget represents a derived check target from a plan. // This is computed at runtime from the plan's specs, not stored. type CheckTarget struct { // Target is the canonical database resource name: instances/{instance}/databases/{database} // or projects/{project}/instances/{instance}/databases/{database}. Target string // SheetSha256 is the content hash of the SQL sheet SheetSha256 string // EnablePriorBackup indicates if backup before migration is enabled EnablePriorBackup bool // EnableGhost indicates if gh-ost online migration is enabled EnableGhost bool // GhostFlags are configuration flags for gh-ost GhostFlags map[string]string // Types are the plan check types to run for this target Types []storepb.PlanCheckType }这解释了为什么最终PlanCheckRunMessage中连Config字段都被移除——配置在运行时由 plan + project + database group 即时推导,行内只保留result。对应迁移目录中的3.14/0021##remove_plan_check_run_config_payload.sql进一步印证了config列的移除。
6.2 采样前移:DeriveCheckTargets
设计文档强调"sampling applied at config generation time, not post-hoc"。当前实现由 derive.go 承载,并提供两个语义不同的推导入口:
// DeriveCheckTargets derives check targets from a plan and optional database // group, applying the project's CI sampling limit: plan checks are CI // validation, and sampling bounds their cost. func DeriveCheckTargets(ctx context.Context, s *store.Store, project *store.ProjectMessage, plan *store.PlanMessage, databaseGroup *v1pb.DatabaseGroup) ([]*CheckTarget, error) { return deriveTargets(ctx, s, project, plan, databaseGroup, true) } // DeriveReviewTargets derives the same targets without CI sampling: a review // run's DONE means every (spec, target) unit was evaluated, so review must // see the full target set. func DeriveReviewTargets(ctx context.Context, s *store.Store, project *store.ProjectMessage, plan *store.PlanMessage, databaseGroup *v1pb.DatabaseGroup) ([]*CheckTarget, error) { return deriveTargets(ctx, s, project, plan, databaseGroup, false) }推导逻辑(deriveTargets)完整对应实现文档 Task 6 中getPlanCheckRunFromPlan的骨架:
- 遍历
plan.Config.Specs,CreateDatabaseConfig与ExportDataConfig不产生检查; ChangeDatabaseConfig若属于 release(Release != "")则跳过检查;- 目标数据库展开:若单个 target 恰好是 database group 名称,则展开为该 group 的
MatchedDatabases,否则直接用Targets; - 采样前移:若
project.Setting.GetCiSamplingSize() > 0且数据库数超限,则截断databases[:samplingSize]; - 从 sheet 内容解析 gh-ost:
ghost.IsGhostEnabled探测gh-ost指令,ghost.ParseGhostDirective解析ghost_flags; - 每个数据库生成一个
CheckTarget,默认类型为STATEMENT_ADVISE + STATEMENT_SUMMARY_REPORT,启用 gh-ost 时追加GHOST_SYNC。
for _, target := range databases { types := []storepb.PlanCheckType{ storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_ADVISE, storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_SUMMARY_REPORT, } if enableGhost { types = append(types, storepb.PlanCheckType_PLAN_CHECK_TYPE_GHOST_SYNC) } targets = append(targets, &CheckTarget{ Target: target, SheetSha256: config.ChangeDatabaseConfig.SheetSha256, EnablePriorBackup: config.ChangeDatabaseConfig.EnablePriorBackup, EnableGhost: enableGhost, GhostFlags: ghostFlags, Types: types, }) }6.3 调度器在运行时完成推导
合并后的调度器 scheduler.go 不再持有 type→executor 的注册表(Register方法被删除),构造函数只接收单个CombinedExecutor:
func NewScheduler(s *store.Store, bus *bus.Bus, executor *CombinedExecutor, licenseService *enterprise.LicenseService, productMetrics *productmetrics.ProductMetrics) *Scheduler { return &Scheduler{ store: s, bus: bus, executor: executor, licenseService: licenseService, productMetrics: productMetrics, } }runPlanCheckRun的执行链变为:
- 通过
ClaimAvailablePlanCheckRuns原子抢占 AVAILABLE 行(FOR UPDATE SKIP LOCKED); - 校验
plan.Config.GetApprovalInputVersion() == approvalInputVersion,不一致则标记 CANCELED(stale run 保护); - 加载 project、database group;
- 调用
DeriveCheckTargets在运行时推导 targets(采样在此生效); - 对每个 target 调用
s.executor.RunForTarget聚合结果; - 调用
markPlanCheckRunDone/Failed/Canceled落库,其中 DONE 后还会向ApprovalCheckChan发送信号触发审批检查。
调度器通过planCheckSchedulerInterval = 5 * time.Second的 ticker 与PlanCheckTickleChan双通道触发,并受 HA 副本数 license 限制(CheckReplicaLimit)约束。
七、消费者更新:审批与自动发布
7.1 审批 runner
设计文档把backend/runner/approval/runner.go的消费逻辑从"按类型查多条"改为"取单条、按结果过滤"。实现文档 Task 7 给出了具体改法:
// Check plan check runs status planCheckRun, err := r.store.GetPlanCheckRun(ctx, plan.UID) if err != nil { return nil, false, errors.Wrapf(err, "failed to get plan check run for plan %v", plan.UID) } // No plan check configured if planCheckRun == nil { // Continue with existing logic for no checks } // Wait for plan check to complete if planCheckRun.Status == store.PlanCheckRunStatusRunning { return nil, false, nil // Not ready yet, retry later } // Build latestPlanCheckRun map from results type Key struct { InstanceID string DatabaseName string } latestPlanCheckRun := map[Key]*storepb.PlanCheckRunResult_Result{} for _, result := range planCheckRun.Result.Results { // Only consider summary report results if result.CheckType != storepb.PlanCheckType_PLAN_CHECK_TYPE_STATEMENT_SUMMARY_REPORT { continue } key := Key{ InstanceID: result.InstanceId, DatabaseName: result.DatabaseName, } latestPlanCheckRun[key] = result }审批流程只关心STATEMENT_SUMMARY_REPORT类型的变更摘要,因此从合并后的结果数组中按checkType过滤出目标子集,按 (instance, database) 建索引。
7.2 自动发布调度器
backend/runner/taskrun/auto_rollout_scheduler.go在RequirePlanCheckNoError项目设置下做门槛检查:只有当 plan check run 状态为 DONE 且结果中不存在Advice_ERROR时才允许自动发布。实现文档 Task 8 给出的逻辑:
planCheckRun, err := s.store.GetPlanCheckRun(ctx, plan.UID) if err != nil { return false, errors.Wrapf(err, "failed to get plan check run") } if planCheckRun == nil { return true, nil // No checks configured } if planCheckRun.Status != store.PlanCheckRunStatusDone { return false, nil } for _, result := range planCheckRun.Result.Results { if result.Status == storepb.Advice_ERROR { return false, nil } } return true, nil注意:实现文档中的代码草案以plan.UID为参数,而当前仓库的GetPlanCheckRun签名已演进为GetPlanCheckRun(ctx, projectID, planUID)(plan_check_run.go),这是落地时随project列引入而做的调整。
八、数据迁移:纯 SQL 聚合
8.1 迁移脚本落地
实现文档 Task 9 的迁移脚本在仓库中已实际落地为 backend/migrator/migration/3.14/0006##consolidate_plan_check_runs.sql(实现文档中的文件名编号为0004,实际提交为0006)。其执行步骤:
Step 1:去重建临时表——按(plan_id, type, config->>'instanceId', config->>'databaseName')取每个维度组合的最新一条(DISTINCT ON+created_at DESC),仅保留最近 30 天且非 CANCELED 的记录:
CREATE TEMP TABLE plan_check_run_deduped AS SELECT DISTINCT ON (plan_id, type, config->>'instanceId', config->>'databaseName') id, plan_id, type, status, config, result, created_at, updated_at FROM plan_check_run WHERE created_at >= NOW() - INTERVAL '30 days' AND status != 'CANCELED' ORDER BY plan_id, type, config->>'instanceId', config->>'databaseName', created_at DESC;Step 2:先删 type 列——与实现文档草案(先 DELETE 再 DROP)不同,实际脚本先ALTER TABLE plan_check_run DROP COLUMN type;再清理数据。
Step 3:清空旧数据:DELETE FROM plan_check_run;
Step 4:聚合插入单条记录——按plan_id分组:
- 状态聚合:任一记录 RUNNING → RUNNING;否则任一 FAILED → FAILED;否则 DONE;
- config:若存在 RUNNING,置空
{"targets": []}(由调度器重跑推导);否则用jsonb_agg聚合 targets,并按类型映射checkTypes数组,target字段拼接为'instances/' || instanceId || '/databases/' || databaseName; - result:RUNNING 时置空
{"results": []};否则用LEFT JOIN LATERAL jsonb_array_elements(result->'results')把每条结果||上instanceId/databaseName/checkType标签,聚合为扁平结果数组。
Step 5:清理临时表:DROP TABLE plan_check_run_deduped;
整个迁移脚本的核心价值在于:无需应用层参与,一次 SQL 事务即可把 N×types 行无损折叠为每 plan 一行,历史检查结论以带标签的扁平结果数组完整保留。
8.2 LATEST.sql 的表结构现状
设计/实现文档中的LATEST.sql是当时的草案(含type注释占位与config列)。当前仓库 backend/migrator/migration/LATEST.sql 的最终形态已完全演进为合并模型:
CREATE TABLE plan_check_run ( -- unique and auto-increase per project id bigint NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), project text NOT NULL REFERENCES project(resource_id), plan_id bigint NOT NULL, status text NOT NULL CHECK (status IN ('AVAILABLE', 'RUNNING', 'DONE', 'FAILED', 'CANCELED')), -- Stored as PlanCheckRunResult (proto/store/store/plan_check_run.proto) result jsonb NOT NULL DEFAULT '{}', PRIMARY KEY (project, id), FOREIGN KEY (project, plan_id) REFERENCES plan(project, id) ); CREATE UNIQUE INDEX idx_plan_check_run_unique_plan_id ON plan_check_run(project, plan_id); CREATE INDEX idx_plan_check_run_active_status ON plan_check_run(status, id) WHERE status IN ('AVAILABLE', 'RUNNING');关键点:
type列与config列均已移除(对应迁移0006##consolidate_plan_check_runs.sql与0021##remove_plan_check_run_config_payload.sql);(project, plan_id)唯一索引idx_plan_check_run_unique_plan_id从数据库层面强制"一 plan 一行";- 部分索引
idx_plan_check_run_active_status只为活跃行(AVAILABLE/RUNNING)服务,加速调度器扫描; - 每个 plan 的检查配置与结果全部编码在
resultJSONB 中,对应 protoPlanCheckRunResult。
九、状态与结果模型
合并后的记录级状态与结果语义在设计文档中有明确约定:
| 状态 | 结果 | 含义 |
|---|---|---|
| RUNNING | 空[] | executor 仍在处理 |
| DONE | 全部结果 | 所有检查完成 |
| FAILED | 部分结果 + 错误 | 基础设施错误,已产生的部分结果予以保留 |
补充约定:
- CANCELED 记录通常被丢弃(少见,用户主动取消);
- 迁移时若存在 RUNNING 记录,合并后置为 RUNNING,由 Bytebase 重新执行——plan check 是幂等的,重跑成本低。
当前仓库状态枚举在此基础上增加了AVAILABLE(等待被调度器认领的中间态,见ClaimAvailablePlanCheckRuns),并配套FailStalePlanCheckRuns超时兜底,这是对设计文档状态模型的落地扩展。
十、API 兼容性
实现文档 Task 10 要求保持对外 API 不变:ListPlanCheckRuns内部把单条合并记录"展开"为客户端期望的按类型分组的虚拟PlanCheckRun列表,客户端无需感知存储模型的变更。涉及文件为backend/api/v1/plan_service.go与backend/api/v1/plan_service_converter.go——对外兼容、对内收敛,是本次重构的底线约束。
十一、风险与缓解
设计文档的原始风险评估如下:
| 风险 | 缓解措施 |
|---|---|
| 迁移时存在进行中的 RUNNING 检查 | 迁移后重跑(开销低,plan check 幂等) |
| API 向后兼容 | ListPlanCheckRuns将单条记录转换为虚拟列表 |
| 顺序执行瓶颈 | 未来可按需演进为并行 goroutine |
从当前仓库看,前两项已按设计落地;顺序执行方面,CombinedExecutor.RunForTarget内部按 target/type 串行执行,但调度器runOnce对多个 plan 的 check run 以go s.runPlanCheckRun(...)并发调度,整体吞吐由"多 plan 并行 + 单 plan 内串行"构成。
十二、实现任务全景与验证
实现文档以 11 个任务组织整个改造,当前仓库中均已可找到对应落点:
| Task | 内容 | 关键文件(仓库现状) |
|---|---|---|
| 1 | Proto 更新 | proto/store/store/plan_check_run.proto |
| 2 | Store 更新 | backend/store/plan_check_run.go |
| 3 | 组合执行器 | backend/runner/plancheck/executor_combined.go |
| 4 | 调度器简化 | backend/runner/plancheck/scheduler.go |
| 5 | Server 注册 | backend/server/server.go |
| 6 | 配置生成(演进为运行时推导) | backend/runner/plancheck/derive.go |
| 7 | 审批消费者 | backend/runner/approval/下审批 runner |
| 8 | 自动发布消费者 | backend/runner/taskrun/auto_rollout_scheduler.go |
| 9 | 数据迁移 | backend/migrator/migration/3.14/0006##consolidate_plan_check_runs.sql |
| 10 | API 兼容层 | backend/api/v1/plan_service*.go |
| 11 | 构建与测试 | go build、golangci-lint run、go test |
实现文档给出的构建与验证命令可直接参考使用:
# 构建后端 go build -ldflags "-w -s" -p=16 -o ./bytebase-build/bytebase ./backend/bin/server/main.go # 全量 lint golangci-lint run --allow-parallel-runners # 定向测试(Store 层与 plancheck 包) go test -v -count=1 github.com/bytebase/bytebase/backend/store -run PlanCheck go test -v -count=1 github.com/bytebase/bytebase/backend/runner/plancheck仓库中backend/store/plan_check_run_test.go、backend/runner/plancheck/scheduler_test.go、backend/runner/plancheck/derive_test.go以及backend/tests/下的集成测试(如approval_test.go、project_instance_scheduler_test.go)共同覆盖了合并模型的读写路径与消费者行为。
结语
"合并计划检查"是 Bytebase 对 plan check 存储与执行模型的一次收敛式重构:从"每 plan N×types 行"到"每 plan 一行 + JSONB 聚合",从"按类型注册的 executor 表"到"单一组合执行器顺序编排",从"事后采样截断"到"配置推导阶段前移采样",再到"纯 SQL 聚合迁移 + API 兼容层"。以设计文档为骨架、实现文档为施工图,再对照仓库源码可以看到,最终落地版本在细节上做了多处务实演进(target资源名字段、config列移除与运行时推导、approvalInputVersion行级并发控制、AVAILABLE中间态与超时兜底),这些演进使得该模型在行数收敛的同时,也具备了更强的并发安全与自愈能力。
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考