256 lines
13 KiB
TypeScript
Vendored
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"]);
|
|
});
|
|
});
|