Bytebase Plan Check Run 运行时派生重构:从存储冗余配置到动态派发的实现全解
【免费下载链接】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/2026-01-05-plan-check-run-runtime-derivation-impl.md编写,并结合仓库中已落地的源码(迁移脚本、proto、store 层、runner/plancheck 包及测试)进行验证与深化。文章聚焦于 "Plan Check Run 配置运行时派生" 这一重构:如何移除plan_check_run表中的config与payload自包含副本,改为调度器在运行时从 Plan 派生检查目标。读者读完可以掌握该重构的完整实施路径、各层改动要点、底层执行与并发安全设计,以及它在 Bytebase 数据库治理流程中的实际作用。
一、重构背景:为什么不再把配置存进 plan_check_run
在 Bytebase 的数据库变更治理流程中,Plan描述一次数据库变更的完整意图(如对哪些数据库执行哪份 SQL、是否开启 gh-ost 在线变更、是否先做备份等),而Plan Check Run是围绕这个 Plan 执行的一系列自动检查(语句评审、影响面摘要、gh-ost 同步检查等),其结果是后续审批与发布决策的依据。
在重构之前,plan_check_run表会额外保存一份config与payload列:
config:存储的是PlanCheckRunConfig(proto/store/store/plan_check_run.proto),其中包含每个检查目标的target(数据库资源名)、sheet_sha256、enable_prior_backup、enable_ghost、ghost_flags与types;payload:存储的是PlanCheckRunResult,用于承载检查结果。
这种"自包含副本"设计的直接问题是数据冗余与一致性漂移:检查配置本质上是从 Plan 的Config.Specs派生出来的,把派生结果再存一份,Plan 一旦变更,副本就可能过期;同时两列 JSONB 也放大了表体积。因此重构的目标非常明确:
移除
config和payload列,由调度器在运行时获取 Plan,并调用一个派生函数(derivation function)在内存中计算出检查目标(CheckTarget),执行器只接收这份运行时派生的配置。
技术栈为 Go + PostgreSQL + Protocol Buffers,共拆分为 9 个可独立提交的任务。下面按任务顺序完整展开,并对照仓库中已落地的实现逐层验证。
二、Task 1:数据库迁移,删除 config 与 payload 列
1. 创建迁移文件
在 backend/migrator/migration/3.14/0021##remove_plan_check_run_config_payload.sql 中创建迁移:
-- Drop config and payload columns from plan_check_run ALTER TABLE plan_check_run DROP COLUMN config; ALTER TABLE plan_check_run DROP COLUMN payload;该文件在仓库中已存在,内容与实现计划完全一致。仓库中还保留了后续演进文件3.14/0022##plan_check_run_ha.sql(HA 相关演进),说明该表在此后还经历了多轮迭代,可见本重构是整个 Plan Check 体系演进的基石之一。
2. 更新 LATEST.sql 中的建表语句
实现计划要求同步修改 backend/migrator/migration/LATEST.sql 中约 213-230 行的建表语句。
变更前:
CREATE TABLE plan_check_run ( id serial PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), plan_id bigint NOT NULL REFERENCES plan(id), status text NOT NULL CHECK (status IN ('RUNNING', 'DONE', 'FAILED', 'CANCELED')), -- Stored as PlanCheckRunConfig (proto/store/store/plan_check_run.proto) config jsonb NOT NULL DEFAULT '{}', -- Stored as PlanCheckRunResult (proto/store/store/plan_check_run.proto) result jsonb NOT NULL DEFAULT '{}', payload jsonb NOT NULL DEFAULT '{}' );变更后:
CREATE TABLE plan_check_run ( id serial PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), plan_id bigint NOT NULL REFERENCES plan(id), status text NOT NULL CHECK (status IN ('RUNNING', 'DONE', 'FAILED', 'CANCELED')), -- Stored as PlanCheckRunResult (proto/store/store/plan_check_run.proto) result jsonb NOT NULL DEFAULT '{}' );注意:从仓库当前 store 层实现 看,PlanCheckRunStatus枚举实际扩展为AVAILABLE / RUNNING / DONE / FAILED / CANCELED五种状态(AVAILABLE用于入队待领取),这表明表结构在后续演进中进一步加入了"可用"状态与 HA 领取机制(详见 Task 7 的调度器部分)。
3. 更新 migrator_test.go 中的版本断言
修改 backend/migrator/migrator_test.go 约 15-16 行的版本校验:
变更前:
require.Equal(t, semver.MustParse("3.14.20"), *files[len(files)-1].version) require.Equal(t, "migration/3.14/0020##remove_task_run_code_and_sheet_sha256.sql", files[len(files)-1].path)变更后:
require.Equal(t, semver.MustParse("3.14.21"), *files[len(files)-1].version) require.Equal(t, "migration/3.14/0021##remove_plan_check_run_config_payload.sql", files[len(files)-1].path)说明:实现计划文档中的提交命令写作
but commit plan-check-run-refactor -m "chore(migration): drop config and payload from plan_check_run",这应当是文档编写时的命令占位/笔误,实际提交时替换为仓库惯用的 git 提交命令即可,分支/feature 名可取plan-check-run-refactor。
三、Task 2:Proto 变更,删除 PlanCheckRunConfig 消息
修改 proto/store/store/plan_check_run.proto(原约 17-29 行),删除PlanCheckRunConfig消息及其嵌套的CheckTarget消息:
message PlanCheckRunConfig { repeated CheckTarget targets = 1; message CheckTarget { // Format: instances/{instance}/databases/{database} string target = 1; string sheet_sha256 = 2; bool enable_prior_backup = 3; bool enable_ghost = 4; map<string, string> ghost_flags = 6; repeated PlanCheckType types = 7; } }删除后,该 proto 文件只保留PlanCheckType枚举与PlanCheckRunResult(及其嵌套的Result、SqlSummaryReport、SqlReviewReport)、ChangedResources等结果承载类型。这与当前仓库中 proto 文件的实际状态一致——PlanCheckRunConfig已不存在,PlanCheckRunResult.Result中为每个结果记录target、type、sheet_sha256与报告内容,用于"合并结果按目标定位"。
随后执行格式化、lint 与重新生成代码:
buf format -w proto && buf lint proto cd proto && buf generatebuf generate会根据proto/buf.gen.yaml重新生成backend/generated-go/store与backend/generated-go/v1下的 Go 代码。
四、Task 3:Store 层更新,PlanCheckRunMessage 去掉 Config
修改 backend/store/plan_check_run.go:
- 结构体瘦身:
PlanCheckRunMessage中删除Config *storepb.PlanCheckRunConfig字段,只保留Result:
// PlanCheckRunMessage is the message for a plan check run. type PlanCheckRunMessage struct { UID int CreatedAt time.Time UpdatedAt time.Time PlanUID int64 Status PlanCheckRunStatus Result *storepb.PlanCheckRunResult }- CreatePlanCheckRun:不再 marshal
create.Config,插入语句也从(plan_id, status, config, result)变为(plan_id, status, result):
func (s *Store) CreatePlanCheckRun(ctx context.Context, create *PlanCheckRunMessage) error { result, err := protojson.Marshal(create.Result) if err != nil { return errors.Wrapf(err, "failed to marshal result") } query := ` INSERT INTO plan_check_run (plan_id, status, result) VALUES ($1, $2, $3) ON CONFLICT (plan_id) DO UPDATE SET status = EXCLUDED.status, result = EXCLUDED.result, updated_at = now() ` if _, err := s.GetDB().ExecContext(ctx, query, create.PlanUID, create.Status, result); err != nil { return errors.Wrapf(err, "failed to upsert plan check run") } return nil }- ListPlanCheckRuns:SELECT 与 Scan 中同步移除
config列,结果仍按result列反序列化为storepb.PlanCheckRunResult。
仓库中的实际状态:Store 层远不止"删列"
对照当前 backend/store/plan_check_run.go,重构后的 store 层在此基础之上又演进出了完整的并发安全与生命周期管理方法,可作为理解该重构设计意图的最佳旁证:
CreatePlanCheckRun升级为事务化 upsert:先对plan_check_run与plan行加FOR UPDATE锁(配合AcquirePlanIssueRolloutAdvisoryLock咨询锁),并通过COALESCE((plan.config->>'approvalInputVersion')::bigint, 0)校验 Plan 的审批输入版本,避免旧版本覆盖新版本;返回值从error变为(bool, error)表示是否真正写入;ClaimAvailablePlanCheckRuns:用FOR UPDATE ... SKIP LOCKED原子地把所有AVAILABLE的 run 标记为RUNNING并返回(含ApprovalInputVersion),支持多副本调度器并发领取互不阻塞;UpdatePlanCheckRunIfApprovalInputVersion/CancelPlanCheckRunIfApprovalInputVersion/RefreshPlanCheckRunIfStaleApprovalInputVersion:所有状态迁移都绑定版本号条件,杜绝"旧 worker 覆盖新行";FailStalePlanCheckRuns:对超时的RUNNINGrun 写入带"Plan check run timed out"的失败结果。
从中可以清晰看到本重构引入的"运行时派生 + 不落盘配置"为后续的版本化并发控制与多副本 HA铺平了道路。
五、Task 4:新增 CheckTarget 派生类型
创建 backend/runner/plancheck/check_target.go,定义纯内存的检查目标结构:
package plancheck import storepb "github.com/bytebase/bytebase/backend/generated-go/store" // 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 }仓库中该文件的注释对Target的格式做了细化:既可以是instances/{instance}/databases/{database},也可以是projects/{project}/instances/{instance}/databases/{database},说明在演进过程中资源命名体系纳入了 project 维度。该结构完全替代了原先持久化的PlanCheckRunConfig,是"配置随用随算、不落库"的核心载体。
六、Task 5:抽取 DeriveCheckTargets 派生函数
创建 backend/runner/plancheck/derive.go,把原先散落在plan_service.go的派生逻辑(getPlanCheckRunFromPlan)抽取为独立的导出函数:
// 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. // This replaces the stored config by computing targets at runtime. 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) }核心派生逻辑(deriveTargets)遍历plan.Config.Specs,按 spec 类型分发:
for _, spec := range plan.Config.Specs { switch config := spec.Config.(type) { case *storepb.PlanConfig_Spec_CreateDatabaseConfig: // No checks for create database. case *storepb.PlanConfig_Spec_ChangeDatabaseConfig: // Skip plan checks for releases. if config.ChangeDatabaseConfig.Release != "" { continue } var databases []string if len(config.ChangeDatabaseConfig.Targets) == 1 && databaseGroup != nil && config.ChangeDatabaseConfig.Targets[0] == databaseGroup.Name { for _, m := range databaseGroup.MatchedDatabases { databases = append(databases, m.Name) } } else { databases = config.ChangeDatabaseConfig.Targets } // Apply sampling upfront if applySampling { if samplingSize := project.Setting.GetCiSamplingSize(); samplingSize > 0 && len(databases) > int(samplingSize) { databases = databases[:samplingSize] } } // Parse ghost config from sheet content var enableGhost bool var ghostFlags map[string]string sheetContent, err := getSheetContent(ctx, s, config.ChangeDatabaseConfig.SheetSha256) if err != nil { return nil, errors.Wrapf(err, "failed to get sheet content") } if sheetContent != "" { enableGhost = ghost.IsGhostEnabled(sheetContent) if enableGhost { ghostFlags, err = ghost.ParseGhostDirective(sheetContent) if err != nil { return nil, errors.Wrapf(err, "failed to parse ghost directive") } } } 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, }) } default: return nil, errors.Errorf("unknown spec config type %T", config) } }与实现计划相比,实际落地版本的关键增强
实现计划中的初版派生逻辑相对朴素,而 derive.go 的当前实现体现了三处重要演进:
- gh-ost 配置改为从 sheet 内容动态解析:不再直接信任
ChangeDatabaseConfig上携带的 ghost 标志,而是通过getSheetContent读取 SQL sheet 的正文(按SheetSha256内容寻址),再调用ghost.IsGhostEnabled与ghost.ParseGhostDirective(backend/component/ghost)判断是否启用 gh-ost 并解析其指令参数。这进一步强化了"运行时从权威来源派生"的设计哲学; - 新增
DeriveReviewTargets(不采样版本):实现计划只提供一个派生入口,实际落地拆成两条路径——Plan Check 属于 CI 校验,要受项目的 CI 采样限制(project.Setting.GetCiSamplingSize())控制成本;而 Review Run 的 DONE 语义要求"每个 (spec, target) 单元都被评估过",因此必须看到完整目标集合、不能采样。二者共享deriveTargets,通过applySampling布尔开关区分; - 派生函数签名带上了
ctx与*store.Store:因为需要在派生期读取 sheet 内容,无法再是纯内存计算。
测试验证
derive_test.go 中TestDeriveReviewTargetsSkipsCISampling直接验证了上述采样语义:当项目CiSamplingSize = 1而 Plan 有 3 个目标库时,DeriveCheckTargets只返回 1 个目标,而DeriveReviewTargets返回完整的 3 个。
同时,backend/api/v1/plan_service.go 中原先的getPlanCheckRunFromPlan被简化为调用plancheck.DeriveCheckTargets,并在plan_service.go的 import 中引入github.com/bytebase/bytebase/backend/runner/plancheck。
七、Task 6:执行器全面转向 CheckTarget
1. Executor 接口收敛
executor.go 中的Executor接口统一为按目标执行:
// Executor is the plan check executor. type Executor interface { // RunForTarget will be called periodically by the plan check scheduler for each target RunForTarget(ctx context.Context, target *CheckTarget) (results []*storepb.PlanCheckRunResult_Result, err error) }2. CombinedExecutor 聚合分发
executor_combined.go 中的CombinedExecutor持有store、sheetManager、dbFactory,其RunForTarget遍历target.Types逐个执行,并对结果统一打上目标与类型标签:
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 }runCheck按PlanCheckType分发到三个子执行器:STATEMENT_ADVISE → StatementAdviseExecutor、STATEMENT_SUMMARY_REPORT → StatementReportExecutor、GHOST_SYNC → GhostSyncExecutor。任何一个检查失败都会生成一条ERROR状态的Result并继续下一个类型,而不是中断整个 run——这与"一个目标可能有多个检查维度"的聚合语义一致。
3. 三个子执行器签名改造
- statement_advise_executor.go:
RunForTarget(ctx, target),内部通过e.store.GetSheetFull(ctx, target.SheetSHA256)读取 SQL sheet,其余逻辑使用target.Target、target.EnablePriorBackup、target.EnableGhost; - statement_report_executor.go:同样按
target.SheetSHA256取 sheet,使用target.Target; - ghost_sync_executor.go:使用
target.Target、target.SheetSHA256与target.GhostFlags。
执行器从此不再关心"配置从哪里来",只面向运行时传入的CheckTarget,职责单一、易于测试。
八、Task 7:调度器改为运行时派生
backend/runner/plancheck/scheduler.go 是本次重构的落点。调度器以 5 秒为周期轮询(planCheckSchedulerInterval = 5 * time.Second),同时监听PlanCheckTickleChan事件通道以便即时唤醒:
func (s *Scheduler) Run(ctx context.Context, wg *sync.WaitGroup) { ticker := time.NewTicker(planCheckSchedulerInterval) defer ticker.Stop() defer wg.Done() for { select { case <-ticker.C: if err := s.licenseService.CheckReplicaLimit(ctx); err != nil { slog.Warn("Plan check scheduler skipped due to HA license restriction", log.BBError(err)) continue } s.runOnce(ctx) case <-s.bus.PlanCheckTickleChan: // 同样先做副本数限制检查,再执行一轮 s.runOnce(ctx) case <-ctx.Done(): return } } }每个轮次runOnce调用store.ClaimAvailablePlanCheckRuns原子领取所有AVAILABLE的 run(FOR UPDATE ... SKIP LOCKED),随后为每个领取到的 run 启动 goroutine 执行runPlanCheckRun。
runPlanCheckRun:从 Plan 到结果的生命周期
runPlanCheckRun完整演示了"运行时派生"的调用链(相比实现计划中的初版,实际代码加入了项目归档检查、过期版本跳过、cancel 函数注册等细节):
func (s *Scheduler) runPlanCheckRun(ctx context.Context, projectID string, uid int64, planUID int64, approvalInputVersion int64) { ctxWithCancel, cancel := context.WithCancel(ctx) defer cancel() planCheckRef := bus.PlanCheckRunRef{ProjectID: projectID, UID: uid, ApprovalInputVersion: approvalInputVersion} s.bus.RunningPlanCheckRunsCancelFunc.Store(planCheckRef, cancel) defer s.bus.RunningPlanCheckRunsCancelFunc.Delete(planCheckRef) // Fetch plan to derive check targets at runtime plan, err := s.store.GetPlan(ctxWithCancel, &store.FindPlanMessage{ProjectID: projectID, UID: &planUID}) if err != nil { s.markPlanCheckRunFailed(ctxWithCancel, projectID, uid, approvalInputVersion, err.Error()) return } if plan == nil { s.markPlanCheckRunFailed(ctxWithCancel, projectID, uid, approvalInputVersion, "plan not found") return } if plan.Config.GetApprovalInputVersion() != approvalInputVersion { slog.Info("skip stale plan check run", ...) s.markPlanCheckRunCanceled(ctxWithCancel, projectID, uid, approvalInputVersion, "stale plan check run") return } project, err := s.store.GetProjectByResourceID(ctxWithCancel, plan.ProjectID) if err != nil { s.markPlanCheckRunFailed(...) return } if project == nil { s.markPlanCheckRunFailed(..., "project not found") return } if project.Deleted { s.markPlanCheckRunCanceled(..., "project is archived") return } // Get database group if needed (for spec expansion) databaseGroup, err := GetDatabaseGroupForPlan(ctxWithCancel, s.store, plan, nil) if err != nil { s.markPlanCheckRunFailed(...) return } // Derive check targets from plan targets, err := DeriveCheckTargets(ctxWithCancel, s.store, project, plan, databaseGroup) if err != nil { s.markPlanCheckRunFailed(...) return } var results []*storepb.PlanCheckRunResult_Result for _, target := range targets { targetResults, targetErr := s.executor.RunForTarget(ctxWithCancel, target) if targetErr != nil { err = targetErr break } results = append(results, targetResults...) } if err != nil { if errors.Is(err, context.Canceled) { s.markPlanCheckRunCanceled(...) } else { s.markPlanCheckRunFailed(...) } } else { s.markPlanCheckRunDone(ctxWithCancel, projectID, uid, planUID, approvalInputVersion, results) } }关键点逐一展开:
- 注册取消函数:以
bus.PlanCheckRunRef{ProjectID, UID, ApprovalInputVersion}为键把 cancel 函数挂到RunningPlanCheckRunsCancelFunc,外部(如计划被编辑/取消时)可以按引用精确取消正在执行的 run; - 运行时取 Plan:通过
s.store.GetPlan拿到最新 Plan,这是派生配置的唯一权威来源; - 过期版本防护:如果 Plan 当前的
approvalInputVersion与领取时记录的版本不一致,说明 Plan 已被修改,本次 run 直接标记为CANCELED("stale plan check run"),等待新一轮 AVAILABLE run 产生; - 项目级校验:项目不存在或已归档(
project.Deleted)时分别 FAILED / CANCELED; - 数据库组展开:通过
GetDatabaseGroupForPlan解析 Plan 中可能指向 DatabaseGroup 的目标,供派生函数做成员展开(实现计划中的convertToDatabaseGroup助手在演进中演进为独立的 database_group.go); - 派生与执行:
DeriveCheckTargets得到目标集合,循环交给CombinedExecutor.RunForTarget;任一目标出错则中断并整单标记 FAILED/CANCELED,全部成功则markPlanCheckRunDone; - 收尾联动:
markPlanCheckRunDone在写入 DONE 结果后,还会查找关联 Issue,并通过s.bus.ApprovalCheckChan触发审批查找(approval finding)——Plan Check 完成后审批流程随即启动,进而触发 rollout 创建。
状态写入统一走UpdatePlanCheckRunIfApprovalInputVersion,该 store 方法在 WHERE 条件中同时校验status = RUNNING与approvalInputVersion匹配,从数据库层面杜绝了过期 worker 覆盖新行的问题。
九、Task 8 与 Task 9:构建、测试与收尾
重构完成后按以下顺序验证与清理:
# 构建后端 go build -ldflags "-w -s" -p=16 -o ./bytebase-build/bytebase ./backend/bin/server/main.go # 对全部改动包执行 lint golangci-lint run --allow-parallel-runners ./backend/store/... ./backend/api/v1/... ./backend/runner/plancheck/... # 提交 lint 修复(如需要) # 全仓最终 lint golangci-lint run --allow-parallel-runners收尾阶段注意检查 backend/api/v1/plan_service.go 中是否还存在对已删除storepb.PlanCheckRunConfig的引用,若有则清理 import。仓库中还保留了 scheduler_test.go 与 derive_test.go 等测试,验证调度与派生的正确性。
十、任务全景与设计收益
| 任务 | 内容 | 关键文件 |
|---|---|---|
| 1 | 数据库迁移,删除config/payload列 | backend/migrator/migration/3.14/0021##remove_plan_check_run_config_payload.sql、LATEST.sql、migrator_test.go |
| 2 | Proto 变更,删除PlanCheckRunConfig | proto/store/store/plan_check_run.proto |
| 3 | Store 层更新 | backend/store/plan_check_run.go |
| 4 | 新增CheckTarget派生类型 | check_target.go |
| 5 | 抽取DeriveCheckTargets | derive.go、plan_service.go |
| 6 | 执行器全面使用CheckTarget | executor.go、executor_combined.go、三个子执行器 |
| 7 | 调度器运行时派生 | scheduler.go |
| 8 | 构建与测试 | go build、golangci-lint |
| 9 | 最终清理与全量 lint | 清理plan_service.go无用 import |
重构带来的核心收益
- 单一事实来源:检查配置只存在于 Plan 本身,任何时刻派生的都是最新语义,Plan 编辑后不再有"副本过期"的隐患;
- 表结构瘦身:
plan_check_run只保留结果列,JSONB 冗余显著下降; - 为版本化并发控制铺路:
approvalInputVersion贯穿领取、执行、写回全链路,配合FOR UPDATE SKIP LOCKED与条件更新,支撑多副本 HA 调度(后续的3.14/0022##plan_check_run_ha.sql即在此基础上演进); - 职责解耦:派生(derive)与执行(executor)分离,
CheckTarget成为二者之间清晰的纯内存契约,便于单测(见 derive_test.go)与后续扩展新的 PlanCheckType。
结语
本文从实现计划出发,结合 Bytebase 仓库中已经落地的源码,完整还原了 Plan Check Run 从"存储自包含配置"到"运行时派生配置"的重构路径。无论你是想理解 Bytebase 数据库治理流水线中 Plan Check 的底层机制,还是要在自己的项目中实践"配置随用随算、不落冗余副本"的架构思路,这条从数据库迁移 → proto → store → 派生函数 → 执行器 → 调度器的改造链路,都是一份可直接参考的完整范本。
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考