mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-04 20:48:50 +00:00
feat: add remote branch diff and merge client APIs (#3686)
This PR adds some support for `diff` / `merge` in the remote client as for local tables we stay `NotSupported` until https://github.com/lance-format/lance/issues/7263. This wires the two review-and-land calls against the remote REST API: - `POST /v1/table/{id}/branches/diff` - `POST /v1/table/{id}/branches/merge` Rust gets typed results (`BranchDiff`, `MergeBranchResult`). Python returns the wire JSON, same shape as the REST response. Merge here means promoting a branch's added columns onto `main`. ### Behavior - Remote only. Local raises `NotSupported`. - A rejected merge is not an exception. HTTP 409 still returns `Ok` / a dict with `status="rejected"` and blockers in `diff.mergeBlockers`. - Unknown blocker / status codes parse as `Unknown` so a newer server does not break older clients. - `MergePreview` tolerates missing fields for the same reason. - Merge requests are not retried. 409 is final and carries the body you need. ### Example ```python table = db.open_table("images") table.branches.create("exp") exp = table.branches.checkout("exp") exp.add_columns({"tag": "cast('draft' as string)"}) diff = table.branches.diff("exp") preview = table.branches.merge("exp", dry_run=True) result = table.branches.merge("exp", dry_run=False) if result["status"] == "merged": print("landed at", result["mainVersionAfter"]) elif result["status"] == "rejected": print(result["diff"]["mergeBlockers"]) ``` ### Testing cargo test -p lancedb --features remote diff_branch cargo test -p lancedb --features remote merge_branch --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -124,6 +124,14 @@ export {
|
||||
export {
|
||||
Table,
|
||||
Branches,
|
||||
BranchColumnSummary,
|
||||
BranchColumnChange,
|
||||
BranchIndexSummary,
|
||||
BranchRowCountSummary,
|
||||
MergeBlocker,
|
||||
BranchDiff,
|
||||
MergePreview,
|
||||
MergeBranchResult,
|
||||
AddDataOptions,
|
||||
UpdateOptions,
|
||||
OptimizeOptions,
|
||||
|
||||
@@ -1329,6 +1329,76 @@ export interface FieldMetadataUpdate {
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
/** Summary of a column in a branch diff. */
|
||||
export interface BranchColumnSummary {
|
||||
name: string;
|
||||
dataType: string;
|
||||
nullable: boolean;
|
||||
}
|
||||
|
||||
/** A column whose definition differs between main and the branch. */
|
||||
export interface BranchColumnChange {
|
||||
name: string;
|
||||
main: BranchColumnSummary;
|
||||
branch: BranchColumnSummary;
|
||||
}
|
||||
|
||||
/** Summary of an index in a branch diff. */
|
||||
export interface BranchIndexSummary {
|
||||
indexName: string;
|
||||
columns: string[];
|
||||
indexType?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** Row-level comparison between main and the branch. */
|
||||
export interface BranchRowCountSummary {
|
||||
unchanged: number;
|
||||
newOnBase: number;
|
||||
newOnBranch: number;
|
||||
staleRecompute: number;
|
||||
inputsChanged: number;
|
||||
deltaAvailable: boolean;
|
||||
}
|
||||
|
||||
/** A reason why a branch cannot currently be merged. */
|
||||
export interface MergeBlocker {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Read-only comparison of a branch against main. */
|
||||
export interface BranchDiff {
|
||||
fromBranch: string;
|
||||
parentVersion: number;
|
||||
mainVersion: number;
|
||||
branchVersion: number;
|
||||
baseMoved: boolean;
|
||||
rowCountMain: number;
|
||||
rowCountBranch: number;
|
||||
rowSummary: BranchRowCountSummary;
|
||||
addedColumns: BranchColumnSummary[];
|
||||
removedColumns: BranchColumnSummary[];
|
||||
changedColumns: BranchColumnChange[];
|
||||
addedIndexes: BranchIndexSummary[];
|
||||
removedIndexes: BranchIndexSummary[];
|
||||
mergeable: boolean;
|
||||
mergeBlockers: MergeBlocker[];
|
||||
}
|
||||
|
||||
/** Changes that would be, or were, promoted by a branch merge. */
|
||||
export interface MergePreview {
|
||||
promotedColumns: string[];
|
||||
}
|
||||
|
||||
/** Result of previewing or attempting a branch merge. */
|
||||
export interface MergeBranchResult {
|
||||
status: "ready" | "rejected" | "notImplemented" | "merged" | "unknown";
|
||||
diff: BranchDiff;
|
||||
preview: MergePreview;
|
||||
mainVersionAfter?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Branch manager for a {@link Table}.
|
||||
*
|
||||
@@ -1381,4 +1451,28 @@ export class Branches {
|
||||
async delete(name: string): Promise<void> {
|
||||
return await this.#inner.delete(name);
|
||||
}
|
||||
|
||||
/** Compare a branch against main without modifying either branch. */
|
||||
async diff(fromBranch: string): Promise<BranchDiff> {
|
||||
return (await this.#inner.diff(fromBranch)) as unknown as BranchDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a branch into main.
|
||||
*
|
||||
* Set `dryRun` to `true` to preview the merge. A rejected merge resolves
|
||||
* with `status: "rejected"` instead of throwing.
|
||||
*
|
||||
* @param fromBranch Branch to merge from.
|
||||
* @param dryRun When true, only preview the merge. Defaults to false.
|
||||
*/
|
||||
async merge(
|
||||
fromBranch: string,
|
||||
dryRun: boolean = false,
|
||||
): Promise<MergeBranchResult> {
|
||||
return (await this.#inner.merge(
|
||||
fromBranch,
|
||||
dryRun,
|
||||
)) as unknown as MergeBranchResult;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user