Bytebase 合并计划检查(Consolidated Plan Check Runs):从行爆炸到单记录模型的设计与实现
2026/9/14 4:05:21 网站建设 项目流程

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.protobackend/store/plan_check_run.gobackend/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 的实际形态更进一步——不仅移除了TypeConfig字段也被移除,原因见第六节"配置改为运行时推导":

// 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):AVAILABLERUNNINGDONEFAILEDCANCELED

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.gobackend/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中支持ProjectIDsPlanUIDsResultStatus(通过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 } }

三个内部方法各自实例化既有的StatementAdviseExecutorStatementReportExecutorGhostSyncExecutor并调用其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的骨架:

  1. 遍历plan.Config.SpecsCreateDatabaseConfigExportDataConfig不产生检查;
  2. ChangeDatabaseConfig若属于 release(Release != "")则跳过检查;
  3. 目标数据库展开:若单个 target 恰好是 database group 名称,则展开为该 group 的MatchedDatabases,否则直接用Targets
  4. 采样前移:若project.Setting.GetCiSamplingSize() > 0且数据库数超限,则截断databases[:samplingSize]
  5. 从 sheet 内容解析 gh-ost:ghost.IsGhostEnabled探测gh-ost指令,ghost.ParseGhostDirective解析ghost_flags
  6. 每个数据库生成一个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的执行链变为:

  1. 通过ClaimAvailablePlanCheckRuns原子抢占 AVAILABLE 行(FOR UPDATE SKIP LOCKED);
  2. 校验plan.Config.GetApprovalInputVersion() == approvalInputVersion,不一致则标记 CANCELED(stale run 保护);
  3. 加载 project、database group;
  4. 调用DeriveCheckTargets运行时推导 targets(采样在此生效);
  5. 对每个 target 调用s.executor.RunForTarget聚合结果;
  6. 调用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.goRequirePlanCheckNoError项目设置下做门槛检查:只有当 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.sql0021##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.gobackend/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内容关键文件(仓库现状)
1Proto 更新proto/store/store/plan_check_run.proto
2Store 更新backend/store/plan_check_run.go
3组合执行器backend/runner/plancheck/executor_combined.go
4调度器简化backend/runner/plancheck/scheduler.go
5Server 注册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
10API 兼容层backend/api/v1/plan_service*.go
11构建与测试go buildgolangci-lint rungo 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.gobackend/runner/plancheck/scheduler_test.gobackend/runner/plancheck/derive_test.go以及backend/tests/下的集成测试(如approval_test.goproject_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询