增量分支合并
一、 功能概述与适用场景
通过 --incremental-oggit 启用增量模式。该模式基于 WAL 逻辑解码生成的增量元数据比较和合并变更,不执行全量业务表扫描。适用于子分支向父分支提交开发、测试变更,以及子分支同步父分支的最新变更。
当前仅支持直接父子 timeline 之间的双向 diff/merge,不支持兄弟分支、非直接父子分支或任意 DAG 多路合并。合并方向根据祖先关系自动判断:
child_to_parent:source 是子分支,target 是父分支。parent_to_child:source 是父分支,target 是子分支。
无论合并方向如何,source 始终表示变更来源,target 始终表示应用目标;ours 固定表示 target,theirs 固定表示 source。
二、 前置条件与启用方式
- 两个分支位于同一 Tenant,且为直接父子 timeline。
- 两端均有可连接的 running endpoint;存在多个 running endpoint 时,显式指定对应 endpoint。
- 两端使用兼容的数据库,连接使用相同的 database 名和数据库用户。target 能通过
postgres_fdw访问 source 元数据,并具有合并所需的对象访问和写入权限。 - 两端均已启用 Oggit,worker 状态为
active。本次合并所需的增量日志必须完整可用。 - 命令使用的数据库应与两端 worker 维护增量元数据的数据库一致。
在创建或启动 endpoint 时通过 --enable-oggit 启用 Oggit。以下为启动命令示例,适用于尚未启动的 endpoint;已运行的 endpoint 应按部署流程安排重启,使配置生效:
neon_local endpoint start --enable-oggit --oggit-database postgres <source_endpoint>
neon_local endpoint start --enable-oggit --oggit-database postgres <target_endpoint>--enable-oggit 管理 neon.oggit_enabled,使 OggitWorker 随 endpoint 启动。--oggit-database 指定解码和维护元数据的数据库;省略时沿用 endpoint 上次保存的选择,没有历史选择时使用 postgres。显式指定 postgres 可以重置此前的数据库选择。
三、 工作机制与增量窗口
两端 OggitWorker 从 WAL 逻辑解码生成结构化事件,分别维护本地 oggit 元数据。target 通过 FDW 读取 source 的元数据,再进行差异分析和合并规划,而不是映射业务表执行全量比较。
主要内部元数据如下:
oggit.state:分支身份、祖先关系、worker 状态和解码进度。oggit.change_log:DML 事件,包括行身份、旧值、新值和变化列。oggit.object_change:DDL、TRUNCATE、SEQUENCE 等对象事件。oggit.merge_history:合并方向、增量窗口、策略、状态和冲突数。oggit.merge_conflict:冲突详情及人工决议。oggit.merge_write_barrier:目标分支写屏障状态。
用户不应直接修改这些内部表,也不应手工绕过写屏障。
命令启动时记录 source 和 target 当前 LSN 作为本次窗口上界,并等待两端 scanned_lsn 追齐。这里冻结的是处理边界,不表示停止两端正常写入。没有历史成功合并时,从子分支 branch_start_lsn 对应的共同基线开始;存在成功合并时,从最近一次 applied 边界继续,并通过合并自身事件标记和提交边界避免重复处理。
窗口内同一行的多次变更会压缩为最终增量,再比较双方变更。因此增量合并不是把历史 SQL 逐条原样重放。合并应用在 target 的一个事务中执行。
四、 增量 diff 命令与结果
neon_local branch diff \
--source-branch dev \
--target-branch main \
--database postgres \
--incremental-oggit该示例假设 dev 是 main 的直接子分支。交换 source 和 target 可以比较父分支同步到子分支的增量。
增量 diff 不修改业务数据,也不创建合并历史、冲突记录或写屏障。命令冻结窗口之后的新写入不进入本次结果;再次执行 diff 时会重新确定窗口。后续 merge 会独立确定窗口,不保证与之前 diff 的窗口完全相同。
行级差异类型包括:
theirs_only:仅 source 存在该变更。ours_only:仅 target 存在该变更。same_change:两侧最终操作和值等价,无需重复应用。mergeable:双方变更可合并,例如对同一行不同列的修改。row_conflict:存在不能直接自动合并的行级冲突。unsupported:变更缺少稳定行身份或不支持安全处理。
对象级结果包括 theirs_object、ours_object 和 object_conflict。这些类型描述增量事件,不等同于全量 diff 的 source_only、target_only 或全量行内容。
五、 增量 merge 命令与策略
子分支合入父分支:
neon_local branch merge \
--source-branch dev \
--target-branch main \
--database postgres \
--incremental-oggit \
--strategy manual父分支同步到子分支:
neon_local branch merge \
--source-branch main \
--target-branch dev \
--database postgres \
--incremental-oggit \
--strategy manual--strategy 默认为 fail,增量模式支持以下四种策略:
fail:发现冲突时失败,执行阶段错误直接返回;不是人工恢复策略。ours:保留 target 的冲突变更,跳过 source 对应的冲突或不可自动处理项;其他可合并变更仍会应用。执行阶段仍可能出错。theirs:对普通行级冲突采用 source 变更;对象冲突、unsupported 或不兼容目标对象变更不能通过该策略强制覆盖,可能进入blocked。manual:将需要人工处理的冲突记录下来并进入blocked,等待决议。没有冲突时正常合并,不会仅因使用manual而阻塞。
manual 仅适用于增量模式。增量 merge 应关注返回的 merge_id、合并状态及冲突信息,不应将全量模式的插入、更新统计示例作为增量输出格式。
六、 DML、行身份与冲突
增量模式可应用 INSERT、UPDATE 和 DELETE:
- INSERT 向 target 插入 source 新增行。
- UPDATE 仅更新实际变化列,避免覆盖 source 未修改而 target 已修改的列。
- DELETE 按行身份删除目标行。与全量模式不同,增量模式能够传播 source 的显式删除事件,但不会仅因某行在 source 当前不存在就删除 target 中该行。
- 双方最终变更等价时不重复应用;双方均删除同一行也视为等价。
行身份支持范围:
- 主键:支持单列及复合主键,按键定位行。
- 唯一键:使用能稳定定位行的 replica identity 唯一索引;不能将任意 UNIQUE 约束都视为已经配置了可用行身份。
REPLICA IDENTITY FULL:使用完整旧行保守匹配,只有 target 唯一匹配一行时才允许自动处理。匹配 0 行或多行时记录冲突。- 无稳定行身份:记录
unsupported_row,不自动合并,theirs也不能强制覆盖。
无主键表可根据实际数据约束配置行身份,例如:
CREATE TABLE public.accounts_by_code (
account_code text NOT NULL,
name text,
UNIQUE (account_code)
);
ALTER TABLE public.accounts_by_code
REPLICA IDENTITY USING INDEX accounts_by_code_account_code_key;没有可用唯一键时,可考虑 ALTER TABLE public.t REPLICA IDENTITY FULL,但仍需满足唯一旧行匹配限制。
常见冲突包括 same_column_update(同一行同一列修改为不同值)、delete_update(一侧删除,另一侧更新或插入同一键)、unsupported_row 和旧行匹配数量异常。target 对同一表存在不兼容对象变更时,source 的 DML 也可能被阻断。
七、 DDL、TRUNCATE 与 Sequence
对象变更能否自动重放取决于安全分类、合并方向和双方是否修改同一对象。以下自动处理规则以变更仅存在于 source、且不存在同对象冲突为前提:
safe_additive:如新增可空列、创建普通索引,两个方向均可自动重放。requires_validation:如新增约束、设置 NOT NULL,两个方向均先在保存点内校验,通过后重放。semantic:如重命名、类型变更以及视图、函数、触发器等语义变化,父合子可自动重放,子合父进入对象冲突,需人工确认。destructive:如 DROP、TRUNCATE,父合子可自动重放,子合父进入对象冲突,需人工确认。unsupported:无法解析、安全重放或缺少重放 SQL 的事件,两个方向均进入对象冲突。
双方修改同一对象且变更不同,记录 object_conflict;等价对象变更不重复处理。上述规则不表示支持任意 DDL,仍受事件采集和数据库执行能力限制。
注意:父分支同步到子分支时,符合条件的 DROP、TRUNCATE 会影响目标对象或数据。执行前应查看增量差异并确认合并方向。
对于由 sequence 管理默认值的列,source 插入行应用到 target 时由目标端默认值生成新值,并维护本次合并内的键映射。后续涉及该键的 DML 根据数据库目录中的外键关系改写引用值。应用完成后修复 target sequence,使其不小于表内当前最大对应值。
键映射不等同于推导所有级联、副作用语义;复杂的级联操作、触发器和函数行为仍依赖目标数据库约束检查或冲突处理。sequence 修复失败会记录 sequence 范围的 apply_error。
八、 人工冲突处理、继续与终止
blocked 表示本次合并尚未完成,需要处理冲突或执行错误。此时:
- 仅 target 开启用户 DML/DDL 写屏障;source 仍可继续写入。
- source 后续新写入不属于本次已确定的窗口。
- 保留供恢复流程使用的 FDW 元数据访问对象,不应手工清理。
- 同一 target 不允许启动新的 active merge,应先完成或终止当前合并。
以下示例中的 <merge_id> 和 <conflict_id> 应替换为实际命令返回的标识。
查看冲突:
neon_local branch conflicts --target-branch main --merge-id <merge_id>逐项选择决议:ours 保留目标变更,theirs 采用来源变更,skip 跳过对应项。以下示例选择 ours,其他决议按实际需要替换:
neon_local branch resolve \
--target-branch main \
--merge-id <merge_id> \
--conflict-id <conflict_id> \
--resolution ours需要自定义处理时,通过 --custom-sql 提供 SQL,而不是在普通会话中绕过目标写屏障:
neon_local branch resolve \
--target-branch main \
--merge-id <merge_id> \
--conflict-id <conflict_id> \
--custom-sql 'UPDATE public.items SET price = 22 WHERE id = 2;'该 SQL 仅适用于确实需要将示例冲突行价格设为 22 的情况;使用前应核对对象、行和业务含义。人工决议不会使不受支持的变更自动变为可安全重放,实际应用仍需通过数据库检查。
处理完所有冲突后继续:
neon_local branch continue --target-branch main --merge-id <merge_id>continue 按原窗口重新构造执行计划,不纳入后续 source 写入。如果检测到 target 出现窗口外用户变更,会拒绝继续;此时需要终止本次合并并重新规划。约束、唯一键、外键、对象缺失或 SQL 执行错误可能在实际应用阶段产生 apply_error,需根据冲突原因重新处理,不能假定 resolve 后一定成功。
放弃本次合并:
neon_local branch abort --target-branch main --merge-id <merge_id>abort 将合并状态置为 aborted 并关闭 target 写屏障,不删除历史冲突记录,也不回滚 source 后续写入。它不是撤销已成功合并数据的命令。
九、 示例:增量修改与人工决议
假设 dev 从 main 直接派生,分支点时双方 public.items 包含 id = 2, name = 'banana', price = 20,表定义同第八节。
- 在
dev将id = 2的name改为banana-dev,在main将该行price改为 25。执行本节增量 diff 后,这类不同列更新可判定为mergeable。使用增量 merge 合入后,目标行同时保留name = 'banana-dev'和price = 25。 - 在上述成功合并之后,继续在
dev将该行price改为 30,在main将其改为 35。再次执行增量 diff/merge 时,仅处理新的窗口;双方同列不同值会产生行级冲突。 - 使用
--strategy manual合并进入blocked,记录返回的merge_id,执行branch conflicts查看该行冲突。 - 使用该冲突的实际
conflict_id执行branch resolve --resolution theirs,然后执行branch continue。在没有其他冲突和执行错误的情况下,目标行price更新为 30。 - 如果不希望应用这次变更,应在继续前执行
branch abort,结束本次合并并解除 target 写屏障。继续成功和终止是两种替代路径,不应将 abort 当作成功合并后的回滚步骤。
父分支同步到子分支时,将命令中的 source 和 target 交换,并将冲突处理命令的 --target-branch 改为 dev。
十、 运行维护与限制说明
增量模式依赖连续、完整的事件元数据和可恢复的逻辑解码进度。可通过 oggit.state 及 endpoint 日志观察 worker 状态和进度:scanned_lsn 表示扫描边界,decode_lsn 表示已解码事件提交边界,required_lsn 用于保护重启所需 WAL。worker 长期失败可能增加 WAL 保留量,应及时排查。
删除子分支后,系统异步回收 root parent 上不再需要的增量事件,按当前存活子分支重新计算安全边界。不要手工删除 oggit 历史记录,以免破坏后续 diff/merge 所需窗口。
第七节的“目标表必须有主键”“不删除目标侧独有行或表”和源独有表复制对象清单仅描述全量模式。增量模式按本节行身份、显式删除事件及 DDL 安全分类处理,不应将全量限制直接套用到增量模式。未指定 --incremental-oggit 时仍执行默认全量模式。