Files
dbx/apps/desktop/src/lib/__tests__/dolt/doltVersionControl.spec.ts
T

256 lines
13 KiB
TypeScript
Vendored

import { describe, expect, it } from "vitest";
import type { QueryResult } from "@/types/database";
import {
doltAddAllSql,
doltCheckoutBranchSql,
doltCommitSql,
doltCreateBranchSql,
doltCreateTagSql,
doltClientSessionScope,
doltDeleteBranchSql,
doltDeleteTagSql,
doltDiffSummarySql,
doltDiscardWorkingTreeSql,
doltGraphEdgePath,
doltGraphEdgeRoute,
doltHardResetSql,
doltLogSql,
doltMergeBranchSql,
doltRevertCommitSql,
doltRefColorIndexes,
doltRefsByCommit,
doltStatusSql,
doltTableChangeFlags,
doltTableChangeKind,
doltTableChangeSymbol,
doltTableDiffCountSql,
doltTableDiffSql,
layoutDoltCommitGraph,
parseDoltBranches,
parseDoltCommits,
parseDoltRowDiff,
parseDoltStatus,
parseDoltTableChanges,
parseDoltTags,
} from "@/lib/dolt/doltVersionControl";
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
return { columns, rows, affected_rows: 0, execution_time_ms: 1 };
}
describe("doltVersionControl", () => {
it("quotes revision and table arguments in generated SQL", () => {
expect(doltLogSql("feature/o'hare", 10)).toBe("SELECT * FROM DOLT_LOG('feature/o''hare', '--parents', '--decorate', 'short') LIMIT 10");
expect(doltLogSql("feature\\o'hare", 10)).toBe("SELECT * FROM DOLT_LOG('feature\\\\o''hare', '--parents', '--decorate', 'short') LIMIT 10");
expect(doltStatusSql()).toBe("SELECT table_name, staged, status FROM dolt_status ORDER BY table_name, staged DESC");
expect(doltAddAllSql()).toBe("CALL DOLT_ADD('.')");
expect(doltCommitSql("fix user's row")).toBe("CALL DOLT_COMMIT('-m', 'fix user''s row')");
expect(doltDiffSummarySql("main", "release'1")).toBe("SELECT * FROM DOLT_DIFF_SUMMARY('main', 'release''1')");
expect(doltTableDiffSql("HEAD", "WORKING", "order'items", 25)).toBe("SELECT * FROM DOLT_DIFF('HEAD', 'WORKING', 'order''items') LIMIT 25");
expect(doltTableDiffSql("HEAD", "WORKING", "order'items", 25, 50)).toBe("SELECT * FROM DOLT_DIFF('HEAD', 'WORKING', 'order''items') LIMIT 25 OFFSET 50");
expect(doltTableDiffCountSql("HEAD", "WORKING", "order'items")).toBe("SELECT COUNT(*) AS row_count FROM DOLT_DIFF('HEAD', 'WORKING', 'order''items')");
expect(doltCreateBranchSql("feature/o'hare", "HEAD")).toBe("CALL DOLT_BRANCH('feature/o''hare', 'HEAD')");
expect(doltMergeBranchSql("release'1")).toBe("CALL DOLT_MERGE('release''1')");
expect(doltCheckoutBranchSql("feature/o'hare")).toBe("CALL DOLT_CHECKOUT('feature/o''hare')");
expect(doltRevertCommitSql("abc'123")).toBe("CALL DOLT_REVERT('abc''123')");
expect(doltHardResetSql("abc'123")).toBe("CALL DOLT_RESET('--hard', 'abc''123')");
expect(doltDiscardWorkingTreeSql()).toBe("CALL DOLT_RESET('--hard', 'HEAD')");
expect(doltCreateTagSql("v1'o", "HEAD")).toBe("CALL DOLT_TAG('v1''o', 'HEAD')");
expect(doltDeleteTagSql("v0'o")).toBe("CALL DOLT_TAG('-d', 'v0''o')");
expect(doltDeleteBranchSql("old'branch")).toBe("CALL DOLT_BRANCH('-d', 'old''branch')");
});
it("builds database-scoped client sessions", () => {
expect(doltClientSessionScope("connection-1", "database_a")).toEqual({
connectionId: "connection-1",
database: "database_a",
clientSessionId: "dolt-version-control:connection-1:database_a",
});
expect(doltClientSessionScope("connection-1", "database_b").clientSessionId).not.toBe(doltClientSessionScope("connection-1", "database_a").clientSessionId);
});
it("parses staged and unstaged working tree changes", () => {
const changes = parseDoltStatus(
result(
["status", "table_name", "staged"],
[
["modified", "orders", true],
["new table", "customers", 0],
],
),
);
expect(changes).toEqual([
{ tableName: "orders", staged: true, status: "modified" },
{ tableName: "customers", staged: false, status: "new table" },
]);
});
it("parses Dolt refs, commits, decorations, and table changes by column name", () => {
const branchRows = result(["hash", "name"], [["abc123", "main"]]);
const tagRows = result(["tag_hash", "tag_name"], [["abc123", "v1.0"]]);
const commitRows = result(["commit_hash", "parents", "committer", "email", "date", "message", "refs"], [["abc123", "def456, 789abc", "Ada", "ada@example.com", "2026-08-17", "Ship it", "HEAD -> main, tag: v1.0"]]);
const changeRows = result(["to_table_name", "from_table_name", "diff_type", "data_change", "schema_change"], [["orders", "orders", "modified", 1, 0]]);
const branches = parseDoltBranches(branchRows, "main");
const tags = parseDoltTags(tagRows);
const commits = parseDoltCommits(commitRows);
expect(branches).toEqual([{ name: "main", hash: "abc123", kind: "branch", active: true }]);
expect(tags[0]).toMatchObject({ name: "v1.0", hash: "abc123", kind: "tag" });
expect(commits[0]).toMatchObject({ hash: "abc123", parents: ["def456", "789abc"], committer: "Ada", message: "Ship it" });
expect(commits[0].refs).toEqual(["main", "tag: v1.0"]);
expect(parseDoltTableChanges(changeRows)).toEqual([{ tableName: "orders", fromTableName: "orders", toTableName: "orders", diffType: "modified", dataChange: true, schemaChange: false }]);
expect(
doltRefsByCommit(commits, [...branches, ...tags])
.get("abc123")
?.map((item) => item.name),
).toEqual(["main", "v1.0"]);
});
it("classifies table changes with reference-style symbols and flags", () => {
const added = { diffType: "CREATE", dataChange: false, schemaChange: true } as const;
const removed = { diffType: "deleted", dataChange: true, schemaChange: true } as const;
const modified = { diffType: "modified", dataChange: true, schemaChange: false } as const;
const schemaOnly = { diffType: "modified", dataChange: false, schemaChange: true } as const;
const metadataOnly = { diffType: "modified", dataChange: false, schemaChange: false } as const;
expect(doltTableChangeKind(added)).toBe("added");
expect(doltTableChangeSymbol(added)).toBe("+");
expect(doltTableChangeFlags(added)).toEqual(["schema"]);
expect(doltTableChangeKind(removed)).toBe("removed");
expect(doltTableChangeSymbol(removed)).toBe("-");
expect(doltTableChangeFlags(removed)).toEqual(["data", "schema"]);
expect(doltTableChangeKind(modified)).toBe("modified");
expect(doltTableChangeSymbol(modified)).toBe("*");
expect(doltTableChangeFlags(modified)).toEqual(["data"]);
expect(doltTableChangeKind(schemaOnly)).toBe("schema");
expect(doltTableChangeSymbol(schemaOnly)).toBe("*");
expect(doltTableChangeFlags(schemaOnly)).toEqual(["schema"]);
expect(doltTableChangeKind(metadataOnly)).toBe("modified");
expect(doltTableChangeFlags(metadataOnly)).toEqual(["metadata"]);
});
it("assigns stable lanes to a merge history", () => {
const commits = [
{ hash: "merge", parents: ["main-parent", "feature-parent"], committer: "", email: "", date: "", message: "merge", refs: ["HEAD -> main"] },
{ hash: "main-parent", parents: ["root"], committer: "", email: "", date: "", message: "main", refs: [] },
{ hash: "feature-parent", parents: ["root"], committer: "", email: "", date: "", message: "feature", refs: ["feature"] },
{ hash: "root", parents: [], committer: "", email: "", date: "", message: "root", refs: ["tag: v1"] },
];
const refs = [
{ name: "main", hash: "merge", kind: "branch" as const, active: true },
{ name: "feature", hash: "feature-parent", kind: "branch" as const, active: false },
{ name: "v1", hash: "root", kind: "tag" as const, active: false },
];
const layout = layoutDoltCommitGraph(commits, refs, "main");
expect(layout.laneCount).toBeGreaterThanOrEqual(2);
expect(layout.rows.map((row) => row.lane)).toEqual([0, 0, 1, 0]);
expect(layout.rows.map((row) => row.nodeRef)).toEqual(["main", "main", "feature", "v1"]);
expect(layout.rows[0].edges).toEqual([
{ fromLane: 0, toLane: 0, colorLane: 0, colorRef: "main" },
{ fromLane: 0, toLane: 1, colorLane: 1, colorRef: "feature" },
]);
expect(layout.rows[1].edges).toContainEqual({ fromLane: 1, toLane: 1, colorLane: 1, colorRef: "feature" });
expect(layout.rows[2].edges).toContainEqual({ fromLane: 1, toLane: 0, colorLane: 1, colorRef: "feature" });
});
it("keeps the active branch first-parent chain in lane zero when side commits are interleaved", () => {
const commits = [
{ hash: "merge", parents: ["main-2", "feature-2"], committer: "", email: "", date: "", message: "merge", refs: ["HEAD -> main"] },
{ hash: "feature-2", parents: ["feature-1"], committer: "", email: "", date: "", message: "feature 2", refs: ["feature"] },
{ hash: "main-2", parents: ["root"], committer: "", email: "", date: "", message: "main 2", refs: [] },
{ hash: "feature-1", parents: ["root"], committer: "", email: "", date: "", message: "feature 1", refs: [] },
{ hash: "root", parents: [], committer: "", email: "", date: "", message: "root", refs: [] },
];
const refs = [
{ name: "main", hash: "merge", kind: "branch" as const, active: true },
{ name: "feature", hash: "feature-2", kind: "branch" as const, active: false },
];
expect(layoutDoltCommitGraph(commits, refs, "main").rows.map((row) => row.lane)).toEqual([0, 1, 0, 1, 0]);
});
it("starts fork edges at the real parent and ends merge edges at the merge commit", () => {
expect(doltGraphEdgeRoute(0, 0, 3, 2)).toBe("direct");
expect(doltGraphEdgeRoute(0, 1, 1, 2)).toBe("direct");
expect(doltGraphEdgeRoute(0, 1, 3, 1)).toBe("fork");
expect(doltGraphEdgeRoute(0, 1, 3, 2)).toBe("merge");
expect(doltGraphEdgePath(18, 105, 18, 15)).toBe("M 18 105 L 18 15");
expect(doltGraphEdgePath(18, 105, 36, 15, "fork")).toBe("M 18 105 L 36 75 L 36 15");
expect(doltGraphEdgePath(18, 105, 54, 15, "fork")).toBe("M 18 105 L 54 75 L 54 15");
expect(doltGraphEdgePath(18, 105, 36, 15, "merge")).toBe("M 18 105 L 18 45 L 36 15");
expect(doltGraphEdgePath(18, 105, 54, 15, "merge")).toBe("M 18 105 L 18 45 L 54 15");
expect(doltGraphEdgePath(18, 45, 36, 15)).toBe("M 18 45 L 36 15");
});
it("deduplicates log decorations against branch and tag records", () => {
const commits = [{ hash: "abc123", parents: [], committer: "", email: "", date: "", message: "", refs: ["HEAD -> main", "tag: v1.0", "refs/tags/v2.0"] }];
const refs = [
{ name: "main", hash: "abc123", kind: "branch" as const, active: true },
{ name: "v1.0", hash: "abc123", kind: "tag" as const, active: false },
];
expect(doltRefsByCommit(commits, refs).get("abc123")).toEqual([refs[0], refs[1], { name: "v2.0", hash: "abc123", kind: "tag", active: false }]);
});
it("assigns ref colors independently of query order", () => {
const first = doltRefColorIndexes(["main", "feature", "v1.0"], 18);
const second = doltRefColorIndexes(["v1.0", "main", "feature"], 18);
expect([...first]).toEqual([...second]);
expect(new Set(first.values()).size).toBe(3);
});
it("pairs from/to diff columns and infers row changes", () => {
const parsed = parseDoltRowDiff(
result(
["from_id", "to_id", "from_name", "to_name", "diff_type", "commit_hash"],
[
[null, 1, null, "Ada", "added", "abc"],
[2, 2, "Old", "New", "modified", "def"],
[3, null, "Removed", null, "removed", "ghi"],
],
),
);
expect(parsed.columns).toEqual(["id", "name"]);
expect(parsed.columnKinds).toEqual(["unchanged", "unchanged"]);
expect(parsed.rows).toEqual([
{ kind: "added", before: [null, null], after: [1, "Ada"], changedColumns: ["id", "name"] },
{ kind: "modified", before: [2, "Old"], after: [2, "New"], changedColumns: ["name"] },
{ kind: "removed", before: [3, "Removed"], after: [null, null], changedColumns: ["id", "name"] },
]);
});
it("accepts qualified, quoted, and suffix-style diff columns", () => {
const parsed = parseDoltRowDiff(result(["d.`id_from`", "d.`id_to`", '"from_name"', '"to_name"', "d.diff_type"], [[1, 1, "Old", "New", "modified"]]));
expect(parsed).toEqual({
columns: ["id", "name"],
columnKinds: ["unchanged", "unchanged"],
rows: [{ kind: "modified", before: [1, "Old"], after: [1, "New"], changedColumns: ["name"] }],
});
});
it("keeps schema-only columns aligned and identifies additions and removals", () => {
const parsed = parseDoltRowDiff(result(["from_id", "to_id", "from_legacy", "to_created_at", "diff_type"], [[1, 1, "old", "new", "modified"]]));
expect(parsed).toEqual({
columns: ["id", "legacy", "created_at"],
columnKinds: ["unchanged", "removed", "added"],
rows: [{ kind: "modified", before: [1, "old", null], after: [1, null, "new"], changedColumns: ["legacy", "created_at"] }],
});
});
it("infers schema columns when Dolt returns both sides with one side empty", () => {
const parsed = parseDoltRowDiff(result(["from_id", "to_id", "from_legacy", "to_legacy", "from_created_at", "to_created_at", "diff_type"], [[1, 1, "old", null, null, "new", "modified"]]), true);
expect(parsed.columnKinds).toEqual(["unchanged", "removed", "added"]);
});
});