From fb5a1742d53745ce36ab3ad02a41fd93341288e6 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 16 Sep 2024 19:18:13 +0300 Subject: [PATCH] branch support --- .../src/lix-plugin/detectConflicts.test.ts | 95 +++++++++ .../sdk2/src/lix-plugin/merge.test.ts | 4 - .../resolveConflictBySelecting.test.ts | 54 ++++- .../src/project/loadProjectFromDirectory.ts | 2 +- lix/packages/sdk/src/change-queue.test.ts | 154 +++++++++++--- lix/packages/sdk/src/commit.test.ts | 199 ------------------ lix/packages/sdk/src/commit.ts | 63 ------ lix/packages/sdk/src/database/createSchema.ts | 37 ++-- lix/packages/sdk/src/database/initDb.test.ts | 17 -- lix/packages/sdk/src/database/initDb.ts | 2 + lix/packages/sdk/src/database/schema.ts | 50 ++--- lix/packages/sdk/src/file-handlers.ts | 107 +++++++--- lix/packages/sdk/src/merge/merge.test.ts | 24 +-- lix/packages/sdk/src/merge/merge.ts | 55 +++-- lix/packages/sdk/src/newLix.test.ts | 19 -- lix/packages/sdk/src/open/openLix.ts | 4 - .../query-utilities/get-leaf-change.test.ts | 27 +++ .../src/query-utilities/get-leaf-change.ts | 25 ++- .../get-lowest-common-ancestor.test.ts | 85 ++++++++ .../get-lowest-common-ancestor.ts | 82 ++++++-- lix/packages/sdk/src/query-utilities/index.ts | 1 - .../is-in-simulated-branch.test.ts | 174 --------------- .../query-utilities/is-in-simulated-branch.ts | 47 ----- 23 files changed, 636 insertions(+), 691 deletions(-) delete mode 100644 lix/packages/sdk/src/commit.test.ts delete mode 100644 lix/packages/sdk/src/commit.ts delete mode 100644 lix/packages/sdk/src/query-utilities/is-in-simulated-branch.test.ts delete mode 100644 lix/packages/sdk/src/query-utilities/is-in-simulated-branch.ts diff --git a/inlang/source-code/sdk2/src/lix-plugin/detectConflicts.test.ts b/inlang/source-code/sdk2/src/lix-plugin/detectConflicts.test.ts index 8452aae6da..f09d71c35d 100644 --- a/inlang/source-code/sdk2/src/lix-plugin/detectConflicts.test.ts +++ b/inlang/source-code/sdk2/src/lix-plugin/detectConflicts.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { test, expect } from "vitest"; import { inlangLixPluginV1 } from "./inlangLixPluginV1.js"; + import { newLixFile, openLixInMemory, @@ -11,6 +12,13 @@ import { test("a create operation should not report a conflict given that the change does not exist in target", async () => { const targetLix = await openLixInMemory({ blob: await newLixFile() }); const sourceLix = await openLixInMemory({ blob: await newLixFile() }); + + const currentBranch = await sourceLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + const changes = await sourceLix.db .insertInto("change") .values([ @@ -26,6 +34,16 @@ test("a create operation should not report a conflict given that the change does ]) .returningAll() .execute(); + + await sourceLix.db + .insertInto("branch_change") + .values({ + branch_id: currentBranch.id, + change_id: "1", + seq: 1, + }) + .execute(); + const conflicts = await inlangLixPluginV1.detectConflicts!({ sourceLix, targetLix, @@ -90,6 +108,17 @@ test("it should report an UPDATE as a conflict if leaf changes are conflicting", const targetLix = await openLixInMemory({ blob: await newLixFile() }); const sourceLix = await openLixInMemory({ blob: await targetLix.toBlob() }); + const currentTargetBranch = await targetLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + const currentSourceBranch = await sourceLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + const commonChanges: NewChange[] = [ { id: "12s", @@ -137,11 +166,43 @@ test("it should report an UPDATE as a conflict if leaf changes are conflicting", .values([...commonChanges, ...changesOnlyInSource]) .execute(); + await sourceLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentSourceBranch.id, + change_id: "12s", + seq: 1, + }, + { + branch_id: currentSourceBranch.id, + change_id: "2qa", + seq: 2, + }, + ]) + .execute(); + await targetLix.db .insertInto("change") .values([...commonChanges, ...changesOnlyInTarget]) .execute(); + await targetLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentTargetBranch.id, + change_id: "12s", + seq: 1, + }, + { + branch_id: currentTargetBranch.id, + change_id: "3sd", + seq: 2, + }, + ]) + .execute(); + const conflicts = await inlangLixPluginV1.detectConflicts!({ leafChangesOnlyInSource: changesOnlyInSource as Change[], sourceLix: sourceLix, @@ -223,6 +284,13 @@ test("it should NOT report an UPDATE as a conflict if the common ancestor is the test("it should NOT report a DELETE as a conflict if the parent of the target and source are identical", async () => { const targetLix = await openLixInMemory({ blob: await newLixFile() }); + + const currentTargetBranch = await targetLix.db + .selectFrom("branch") + .where("active", "=", true) + .selectAll() + .executeTakeFirstOrThrow(); + await targetLix.db .insertInto("change") .values([ @@ -240,6 +308,15 @@ test("it should NOT report a DELETE as a conflict if the parent of the target an ]) .execute(); + await targetLix.db + .insertInto("branch_change") + .values({ + branch_id: currentTargetBranch.id, + change_id: "12s", + seq: 1, + }) + .execute(); + const sourceLix = await openLixInMemory({ blob: await targetLix.toBlob() }); const changesNotInTarget: NewChange[] = [ @@ -270,8 +347,26 @@ test("it should NOT report a DELETE as a conflict if the parent of the target an await sourceLix.db.insertInto("change").values(changesNotInTarget).execute(); + await sourceLix.db + .insertInto("branch_change") + .values({ + branch_id: currentTargetBranch.id, + change_id: "3sd", + seq: 2, + }) + .execute(); + await targetLix.db.insertInto("change").values(changesNotInSource).execute(); + await targetLix.db + .insertInto("branch_change") + .values({ + branch_id: currentTargetBranch.id, + change_id: "2qa", + seq: 2, + }) + .execute(); + const conflicts = await inlangLixPluginV1.detectConflicts!({ sourceLix, targetLix, diff --git a/inlang/source-code/sdk2/src/lix-plugin/merge.test.ts b/inlang/source-code/sdk2/src/lix-plugin/merge.test.ts index 3a9ed3e25f..9757217856 100644 --- a/inlang/source-code/sdk2/src/lix-plugin/merge.test.ts +++ b/inlang/source-code/sdk2/src/lix-plugin/merge.test.ts @@ -31,7 +31,6 @@ test("it should update the variant to the source's value", async () => { }), // @ts-expect-error - https://github.com/opral/inlang-message-sdk/issues/123 meta: JSON.stringify(undefined), - commit_id: "c8ad005b-a834-4ca3-84fb-9627546f2eba", }, { id: "24f74fec-fc8a-4c68-b31c-d126417ce3af", @@ -50,7 +49,6 @@ test("it should update the variant to the source's value", async () => { }), // @ts-expect-error - https://github.com/opral/inlang-message-sdk/issues/123 meta: JSON.stringify(undefined), - commit_id: "c8ad005b-a834-4ca3-84fb-9627546f2eba", }, { id: "aaf0ec32-0c7f-4d07-af8c-922ce382aef1", @@ -73,7 +71,6 @@ test("it should update the variant to the source's value", async () => { }), // @ts-expect-error - https://github.com/opral/inlang-message-sdk/issues/123 meta: JSON.stringify(undefined), - commit_id: "c8ad005b-a834-4ca3-84fb-9627546f2eba", }, ]; @@ -102,7 +99,6 @@ test("it should update the variant to the source's value", async () => { meta: JSON.stringify({ id: "6a860f96-0cf3-477c-80ad-7893d8fde852", }), - commit_id: "df455c78-b5ed-4df0-9259-7bb694c9d755", }, ]; diff --git a/inlang/source-code/sdk2/src/lix-plugin/resolveConflictBySelecting.test.ts b/inlang/source-code/sdk2/src/lix-plugin/resolveConflictBySelecting.test.ts index 8d832631f8..6a26e5d272 100644 --- a/inlang/source-code/sdk2/src/lix-plugin/resolveConflictBySelecting.test.ts +++ b/inlang/source-code/sdk2/src/lix-plugin/resolveConflictBySelecting.test.ts @@ -2,10 +2,7 @@ import { test, expect } from "vitest"; import { newProject } from "../project/newProject.js"; import { loadProjectInMemory } from "../project/loadProjectInMemory.js"; -import { - isInSimulatedCurrentBranch, - resolveConflictBySelecting, -} from "@lix-js/sdk"; +import { resolveConflictBySelecting } from "@lix-js/sdk"; test("it should resolve a conflict with the selected change", async () => { const project = await loadProjectInMemory({ blob: await newProject() }); @@ -122,6 +119,48 @@ test("it should resolve a conflict with the selected change", async () => { .returningAll() .execute(); + const currentBranch = await project.lix.db + .selectFrom("branch") + .select("id") + .where("active", "=", true) + .executeTakeFirstOrThrow(); + + await project.lix.db + .insertInto("branch_change") + .values([ + { + id: "branch-change-1", + change_id: changes[0]?.id, + branch_id: currentBranch.id, + seq: 0, + }, + { + id: "branch-change-2", + change_id: changes[1]?.id, + branch_id: currentBranch.id, + seq: 1, + }, + { + id: "branch-change-3", + change_id: changes[2]?.id, + branch_id: currentBranch.id, + seq: 2, + }, + { + id: "branch-change-4", + change_id: "samuels-change", + branch_id: currentBranch.id, + seq: 3, + }, + { + id: "branch-change-5", + change_id: "peters-change", + branch_id: currentBranch.id, + seq: 4, + }, + ]) + .execute(); + const conflicts = await project.lix.db .insertInto("conflict") .values([ @@ -143,15 +182,18 @@ test("it should resolve a conflict with the selected change", async () => { await project.lix.settled(); const changesInCurrentBranch = await project.lix.db - .selectFrom("change") + .selectFrom("branch_change") + .leftJoin("change", "change.id", "branch_change.change_id") .selectAll() - .where(isInSimulatedCurrentBranch) + .where("branch_id", "=", currentBranch.id) + .orderBy("seq") .execute(); expect(changesInCurrentBranch.map((c) => c.id)).toEqual([ changes[0]?.id, changes[1]?.id, changes[2]?.id, + "samuels-change", "peters-change", ]); }); diff --git a/inlang/source-code/sdk2/src/project/loadProjectFromDirectory.ts b/inlang/source-code/sdk2/src/project/loadProjectFromDirectory.ts index 02f8c28b7c..9bfff68bad 100644 --- a/inlang/source-code/sdk2/src/project/loadProjectFromDirectory.ts +++ b/inlang/source-code/sdk2/src/project/loadProjectFromDirectory.ts @@ -419,4 +419,4 @@ export class ResourceFileImportError extends Error { this.cause = args.cause; this.path = args.path; } -} \ No newline at end of file +} diff --git a/lix/packages/sdk/src/change-queue.test.ts b/lix/packages/sdk/src/change-queue.test.ts index 4a2b9d50bd..d04653f0c7 100644 --- a/lix/packages/sdk/src/change-queue.test.ts +++ b/lix/packages/sdk/src/change-queue.test.ts @@ -75,10 +75,22 @@ test("should use queue and settled correctly", async () => { providePlugins: [mockPlugin], }); + const branches = await lix.db.selectFrom("branch").selectAll().execute(); + + expect(branches).toEqual([ + { + id: branches[0]?.id, + name: "main", + base_branch: null, + active: 1, + }, + ]); + const enc = new TextEncoder(); + await lix.db .insertInto("file") - .values({ id: "test", path: "test.txt", data: enc.encode("test") }) + .values({ id: "test", path: "test.txt", data: enc.encode("test orig") }) .execute(); const internalFiles = await lix.db @@ -89,6 +101,7 @@ test("should use queue and settled correctly", async () => { expect(internalFiles).toEqual([]); const queue = await lix.db.selectFrom("change_queue").selectAll().execute(); + expect(queue).toEqual([ { id: 1, @@ -131,15 +144,34 @@ test("should use queue and settled correctly", async () => { plugin_key: "mock-plugin", value: { id: "test", - text: "test", + text: "test orig", }, meta: null, - commit_id: null, operation: "create", }, ]); - // Test replacing uncommitted changes and multiple changes processing + const branchChanges = await lix.db + .selectFrom("branch_change") + .selectAll() + .execute(); + + expect(branchChanges).toEqual([ + { + branch_id: branches[0]?.id, + change_id: changes[0]?.id, + id: branchChanges[0]?.id, + seq: 1, + }, + ]); + + await lix.db + .updateTable("file") + .set({ data: enc.encode("test updated text") }) + .where("id", "=", "test") + .execute(); + + // repeat same update await lix.db .updateTable("file") .set({ data: enc.encode("test updated text") }) @@ -175,6 +207,13 @@ test("should use queue and settled correctly", async () => { metadata: null, data: queue2[1]?.data, }, + { + id: 5, + file_id: "test", + path: "test.txt", + metadata: null, + data: queue2[2]?.data, + }, ]); await lix.settled(); @@ -190,56 +229,123 @@ test("should use queue and settled correctly", async () => { expect(updatedChanges).toEqual([ { + id: changes[0]?.id, author: null, - id: updatedChanges[0]?.id, - created_at: updatedChanges[0]?.created_at, + created_at: changes[0]?.created_at, parent_id: null, type: "text", file_id: "test", - operation: "create", plugin_key: "mock-plugin", value: { id: "test", - text: "test", + text: "test orig", }, meta: null, - commit_id: null, + operation: "create", }, { author: null, - commit_id: null, + id: updatedChanges[1]?.id, created_at: updatedChanges[1]?.created_at, + parent_id: changes[0]?.id, + type: "text", file_id: "test", - id: updatedChanges[1]?.id, - meta: null, operation: "update", - parent_id: updatedChanges[0]?.id, plugin_key: "mock-plugin", - type: "text", value: { id: "test", text: "test updated text", }, + meta: null, }, { + id: updatedChanges[2]?.id, author: null, - commit_id: null, - created_at: updatedChanges[2]?.created_at, + parent_id: updatedChanges[1]?.id, + type: "text", file_id: "test", - id: updatedChanges[2]?.id, - meta: null, + plugin_key: "mock-plugin", operation: "update", - parent_id: updatedChanges[1]?.id, + value: { id: "test", text: "test updated text second update" }, + meta: null, + created_at: updatedChanges[2]?.created_at, + }, + ]); + + const branchChanges2 = await lix.db + .selectFrom("branch_change") + .selectAll() + .execute(); + + expect(branchChanges2).toEqual([ + { + branch_id: branches[0]?.id, + change_id: updatedChanges[0]?.id, + id: branchChanges2[0]?.id, + seq: 1, + }, + { + branch_id: branches[0]?.id, + change_id: updatedChanges[1]?.id, + id: branchChanges2[1]?.id, + seq: 2, + }, + { + branch_id: branches[0]?.id, + change_id: updatedChanges[2]?.id, + id: branchChanges2[2]?.id, + seq: 3, + }, + ]); + + // test change_view + + const changesFromView = await lix.db + .selectFrom("change_view") + .where("branch_id", "=", branches[0]!.id) + .selectAll() + .execute(); + + expect(changesFromView).toEqual([ + { + author: null, + id: changesFromView[0]?.id, + parent_id: null, + type: "text", plugin_key: "mock-plugin", + operation: "create", + value: { id: "test", text: "test orig" }, + meta: null, + created_at: changesFromView[0]?.created_at, + branch_id: branches[0]?.id, + }, + { + author: null, + id: changesFromView[1]?.id, + parent_id: changesFromView[0]?.id, type: "text", - value: { - id: "test", - text: "test updated text second update", - }, + plugin_key: "mock-plugin", + operation: "update", + value: { id: "test", text: "test updated text" }, + meta: null, + created_at: changesFromView[1]?.created_at, + branch_id: branches[0]?.id, + }, + { + author: null, + id: changesFromView[2]?.id, + parent_id: changesFromView[1]?.id, + type: "text", + plugin_key: "mock-plugin", + operation: "update", + value: { id: "test", text: "test updated text second update" }, + meta: null, + created_at: changesFromView[2]?.created_at, + branch_id: branches[0]?.id, }, ]); - await lix.commit({ description: "test commit" }); + }); test("changes should contain the author", async () => { diff --git a/lix/packages/sdk/src/commit.test.ts b/lix/packages/sdk/src/commit.test.ts deleted file mode 100644 index 376a47ed7d..0000000000 --- a/lix/packages/sdk/src/commit.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { expect, test } from "vitest"; -import { openLixInMemory } from "./open/openLixInMemory.js"; -import { newLixFile } from "./newLix.js"; -import type { LixPlugin } from "./plugin.js"; - -test("should be able to add and commit changes", async () => { - const mockPlugin: LixPlugin = { - key: "mock-plugin", - glob: "*", - diff: { - file: async ({ old }) => { - return [ - !old - ? { - type: "text", - operation: "create", - old: undefined, - neu: { - id: "test", - text: "inserted text", - }, - } - : { - type: "text", - operation: "update", - old: { - id: "test", - text: "inserted text", - }, - neu: { - id: "test", - text: "updated text", - }, - }, - ]; - }, - }, - }; - const lix = await openLixInMemory({ - blob: await newLixFile(), - providePlugins: [mockPlugin], - }); - - const firstRef = await lix.db - .selectFrom("ref") - .selectAll() - .where("name", "=", "current") - .executeTakeFirstOrThrow(); - expect(firstRef.commit_id).toBe("00000000-0000-0000-0000-000000000000"); - - const enc = new TextEncoder(); - - await lix.db - .insertInto("file") - .values({ id: "test", path: "test.txt", data: enc.encode("test") }) - .execute(); - - await lix.settled(); - - const changes = await lix.db.selectFrom("change").selectAll().execute(); - - // console.log(await lix.db.selectFrom("queue").selectAll().execute()); - - expect(changes).toEqual([ - { - id: changes[0]?.id, - author: null, - created_at: changes[0]?.created_at, - parent_id: null, - type: "text", - file_id: "test", - plugin_key: "mock-plugin", - value: { - id: "test", - text: "inserted text", - }, - meta: null, - commit_id: null, - operation: "create", - }, - ]); - - await lix.commit({ description: "test" }); - - const secondRef = await lix.db - .selectFrom("ref") - .selectAll() - .where("name", "=", "current") - .executeTakeFirstOrThrow(); - - expect(secondRef.commit_id).not.toBe("00000000-0000-0000-0000-000000000000"); - - const commits = await lix.db.selectFrom("commit").selectAll().execute(); - const commitedChanges = await lix.db - .selectFrom("change") - .selectAll() - .execute(); - - expect(commitedChanges).toEqual([ - { - id: commitedChanges[0]?.id, - author: null, - created_at: changes[0]?.created_at, - parent_id: null, - type: "text", - file_id: "test", - plugin_key: "mock-plugin", - value: { - id: "test", - text: "inserted text", - }, - meta: null, - commit_id: commits[0]?.id!, - operation: "create", - }, - ]); - - expect(commits).toEqual([ - { - id: commits[0]?.id!, - author: null, - created: commits[0]?.created!, - created_at: commits[0]?.created_at!, - description: "test", - parent_id: "00000000-0000-0000-0000-000000000000", - }, - ]); - - await lix.db - .updateTable("file") - .set({ data: enc.encode("test updated text") }) - .where("id", "=", "test") - .execute(); - - await lix.settled(); - - const updatedChanges = await lix.db - .selectFrom("change") - .selectAll() - .execute(); - - expect(updatedChanges).toEqual([ - { - id: updatedChanges[0]?.id!, - author: null, - created_at: updatedChanges[0]?.created_at, - parent_id: null, - type: "text", - file_id: "test", - plugin_key: "mock-plugin", - value: { - id: "test", - text: "inserted text", - }, - meta: null, - commit_id: commits[0]?.id, - operation: "create", - }, - { - id: updatedChanges[1]?.id!, - author: null, - parent_id: updatedChanges[0]?.id!, - created_at: updatedChanges[0]?.created_at, - type: "text", - file_id: "test", - plugin_key: "mock-plugin", - value: { - id: "test", - text: "updated text", - }, - meta: null, - commit_id: null, - operation: "update", - }, - ]); - - await lix.commit({ description: "test 2" }); - const newCommits = await lix.db.selectFrom("commit").selectAll().execute(); - expect(newCommits).toEqual([ - { - id: newCommits[0]?.id!, - author: null, - created: commits[0]?.created!, - created_at: newCommits[0]?.created_at!, - description: "test", - parent_id: "00000000-0000-0000-0000-000000000000", - }, - { - id: newCommits[1]?.id!, - author: null, - created: commits[0]?.created!, - created_at: newCommits[1]?.created_at!, - description: "test 2", - parent_id: newCommits[0]?.id!, - }, - ]); -}); diff --git a/lix/packages/sdk/src/commit.ts b/lix/packages/sdk/src/commit.ts deleted file mode 100644 index 9fe8425772..0000000000 --- a/lix/packages/sdk/src/commit.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Kysely } from "kysely"; -import type { LixDatabaseSchema } from "./database/schema.js"; -import { v4 } from "uuid"; - -export async function commit(args: { - db: Kysely; - currentAuthor?: string; - // TODO remove description - description: string; -}) { - const newCommitId = v4(); - - return args.db.transaction().execute(async (trx) => { - const { commit_id: parent_id } = await trx - .selectFrom("ref") - .select("commit_id") - .where("name", "=", "current") - .executeTakeFirstOrThrow(); - - const commit = await trx - .insertInto("commit") - .values({ - id: newCommitId, - author: args.currentAuthor, - // todo - use zoned datetime - description: args.description, - parent_id, - }) - .returning("id") - .executeTakeFirstOrThrow(); - - await trx - .updateTable("ref") - .where("name", "=", "current") - .set({ commit_id: newCommitId }) - .execute(); - - // if (!args.merge) { - // // look for all conflicts and remove them if there is a new change that has same id - // // TODO: needs checking of type and plugin? and integrate into one sql statement - // const newChanges = ( - // await trx.selectFrom("change").where("commit_id", "is", null).selectAll().execute() - // ).map((change) => change.value.id) - - // await trx - // .updateTable("change") - // .set({ - // conflict: null, - // }) - // .where("conflict", "is not", null) - // .where((eb) => eb.ref("value", "->>").key("id"), "in", newChanges) - // .execute() - // } - - return await trx - .updateTable("change") - .where("commit_id", "is", null) - .set({ - commit_id: commit.id, - }) - .execute(); - }); -} diff --git a/lix/packages/sdk/src/database/createSchema.ts b/lix/packages/sdk/src/database/createSchema.ts index 6590197896..af531f4fcf 100644 --- a/lix/packages/sdk/src/database/createSchema.ts +++ b/lix/packages/sdk/src/database/createSchema.ts @@ -2,13 +2,26 @@ import { sql, type Kysely } from "kysely"; export async function createSchema(args: { db: Kysely }) { return await sql` - CREATE TABLE ref ( - name TEXT PRIMARY KEY, - commit_id TEXT - ); - CREATE TABLE file_internal ( + CREATE TABLE branch ( id TEXT PRIMARY KEY DEFAULT (uuid_v4()), + name TEXT NOT NULL UNIQUE, + active INTEGER DEFAULT FALSE, + base_branch TEXT + ) strict; + + CREATE TABLE branch_change ( + id TEXT DEFAULT (uuid_v4()), + change_id TEXT NOT NULL, + branch_id TEXT NOT NULL, + seq INTEGER NOT NULL + ) strict; + + -- random uuid as the function is not available yet on creating the schema + INSERT INTO branch(id, name, active) values('961582e6-ac9a-480c-ab62-4792821318fc', 'main', true); + + CREATE TABLE file_internal ( + id TEXT PRIMARY KEY DEFAULT (uuid_v4()), path TEXT NOT NULL UNIQUE, data BLOB NOT NULL, metadata TEXT -- Added metadata field @@ -37,10 +50,11 @@ export async function createSchema(args: { db: Kysely }) { operation TEXT NOT NULL, value TEXT, meta TEXT, - commit_id TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL ) strict; + CREATE VIEW change_view AS SELECT author, change.id as id, change.parent_id as parent_id, type, plugin_key, operation, value, meta, created_at, branch_change.branch_id as branch_id FROM branch_change LEFT JOIN change ON branch_change.change_id = change.id ORDER BY seq; + CREATE TABLE conflict ( change_id TEXT NOT NULL, conflicting_change_id TEXT NOT NULL, @@ -49,17 +63,6 @@ export async function createSchema(args: { db: Kysely }) { resolved_with_change_id TEXT, PRIMARY KEY (change_id, conflicting_change_id) ) strict; - - CREATE TABLE 'commit' ( - id TEXT PRIMARY KEY DEFAULT (uuid_v4()), - author TEXT, - parent_id TEXT NOT NULL, - description TEXT NOT NULL, - created TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL - ) strict; - - INSERT INTO ref values ('current', '00000000-0000-0000-0000-000000000000'); CREATE TRIGGER file_update INSTEAD OF UPDATE ON file BEGIN diff --git a/lix/packages/sdk/src/database/initDb.test.ts b/lix/packages/sdk/src/database/initDb.test.ts index d5d779398f..431d04d1e9 100644 --- a/lix/packages/sdk/src/database/initDb.test.ts +++ b/lix/packages/sdk/src/database/initDb.test.ts @@ -20,22 +20,6 @@ test("file ids should default to uuid", async () => { expect(validate(file.id)).toBe(true); }); -test("commit ids should default to uuid", async () => { - const sqlite = await createInMemoryDatabase({ - readOnly: false, - }); - const db = initDb({ sqlite }); - await createSchema({ db }); - - const commit = await db - .insertInto("commit") - .values({ parent_id: "mock", description: "mock" }) - .returningAll() - .executeTakeFirstOrThrow(); - - expect(validate(commit.id)).toBe(true); -}); - test("change ids should default to uuid", async () => { const sqlite = await createInMemoryDatabase({ readOnly: false, @@ -46,7 +30,6 @@ test("change ids should default to uuid", async () => { const change = await db .insertInto("change") .values({ - commit_id: "mock", type: "file", file_id: "mock", plugin_key: "mock-plugin", diff --git a/lix/packages/sdk/src/database/initDb.ts b/lix/packages/sdk/src/database/initDb.ts index 18876f8fca..07f9285da1 100644 --- a/lix/packages/sdk/src/database/initDb.ts +++ b/lix/packages/sdk/src/database/initDb.ts @@ -6,12 +6,14 @@ import type { LixDatabaseSchema } from "./schema.js"; export function initDb(args: { sqlite: SqliteDatabase }) { initDefaultValueFunctions({ sqlite: args.sqlite }); + const db = new Kysely({ dialect: createDialect({ database: args.sqlite, }), plugins: [new ParseJSONResultsPlugin(), new SerializeJsonPlugin()], }); + return db; } diff --git a/lix/packages/sdk/src/database/schema.ts b/lix/packages/sdk/src/database/schema.ts index 9ee8d2a044..ce04c39855 100644 --- a/lix/packages/sdk/src/database/schema.ts +++ b/lix/packages/sdk/src/database/schema.ts @@ -4,19 +4,29 @@ import type { LixPlugin } from "../plugin.js"; export type LixDatabaseSchema = { file: LixFileTable; change: ChangeTable; - commit: CommitTable; - ref: RefTable; + change_view: ChangeView; + branch: BranchTable; + branch_change: BranchChangeMappingTable; file_internal: LixFileTable; change_queue: ChangeQueueTable; conflict: ConflictTable; }; -export type Ref = Selectable; -export type NewRef = Insertable; -export type RefUpdate = Updateable; -type RefTable = { +export type Ref = Selectable; +export type NewRef = Insertable; +export type RefUpdate = Updateable; +type BranchTable = { + id: Generated; name: string; - commit_id: string; + base_branch?: string; + active: boolean; +}; + +type BranchChangeMappingTable = { + id: Generated; + change_id: ChangeTable["id"]; + branch_id: BranchTable["id"]; + seq: number; }; export type ChangeQueueEntry = Selectable; @@ -41,25 +51,6 @@ type LixFileTable = { metadata: Record | null; }; -export type Commit = Selectable; -export type NewCommit = Insertable; -export type CommitUpdate = Updateable; -type CommitTable = { - id: Generated; - // todo: - // multiple authors can commit one change - // think of real-time collaboration scenarios - author?: string; - description: string; - /** - * @deprecated use created_at instead - * todo remove before release - */ - created: Generated; - created_at: Generated; - parent_id: string; -}; - export type Change = Selectable; export type NewChange = Insertable; export type ChangeUpdate = Updateable; @@ -68,11 +59,6 @@ type ChangeTable = { parent_id?: ChangeTable["id"]; author?: string; file_id: string; - /** - * If no commit id exists on a change, - * the change is considered uncommitted. - */ - commit_id?: CommitTable["id"]; /** * The plugin key that contributed the change. * @@ -117,6 +103,8 @@ type ChangeTable = { created_at: Generated; }; +export type ChangeView = ChangeTable & { branch_id: BranchTable["id"] }; + export type Conflict = Selectable; export type NewConflict = Insertable; export type ConflictUpdate = Updateable; diff --git a/lix/packages/sdk/src/file-handlers.ts b/lix/packages/sdk/src/file-handlers.ts index 581afebeef..d3fff1b4dd 100644 --- a/lix/packages/sdk/src/file-handlers.ts +++ b/lix/packages/sdk/src/file-handlers.ts @@ -2,8 +2,7 @@ import { v4 } from "uuid"; import type { LixDatabaseSchema, LixFile } from "./database/schema.js"; import type { LixPlugin } from "./plugin.js"; import { minimatch } from "minimatch"; -import { Kysely } from "kysely"; -import { getLeafChange } from "./query-utilities/get-leaf-change.js"; +import { Kysely, sql } from "kysely"; // start a new normalize path function that has the absolute minimum implementation. function normalizePath(path: string) { @@ -23,7 +22,6 @@ export async function handleFileInsert(args: { }) { const pluginDiffs: any[] = []; - // console.log({ args }); for (const plugin of args.plugins) { // glob expressions are expressed relative without leading / but path has leading / if (!minimatch(normalizePath(args.neu.path), "/" + plugin.glob)) { @@ -34,20 +32,26 @@ export async function handleFileInsert(args: { old: undefined, neu: args.neu, }); - // console.log({ diffs }); pluginDiffs.push({ diffs, pluginKey: plugin.key }); } await args.db.transaction().execute(async (trx) => { + const currentBranch = await trx + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + for (const { diffs, pluginKey } of pluginDiffs) { for (const diff of diffs ?? []) { const value = diff.neu ?? diff.old; + const changeId = v4(); await trx .insertInto("change") .values({ - id: v4(), + id: changeId, type: diff.type, file_id: args.neu.id, author: args.currentAuthor, @@ -60,6 +64,25 @@ export async function handleFileInsert(args: { // add queueId interesting for debugging or knowning what changes were generated in same worker run }) .execute(); + + const previousBranchChange = await trx + .selectFrom("branch_change") + .selectAll() + .where("branch_id", "=", currentBranch.id) + .orderBy("seq", "desc") + .executeTakeFirst(); + + const lastSeq = previousBranchChange?.seq ?? 0; + + await trx + .insertInto("branch_change") + .values({ + id: v4(), + seq: lastSeq + 1, + branch_id: currentBranch.id, + change_id: changeId, + }) + .execute(); } } @@ -102,6 +125,12 @@ export async function handleFileChange(args: { } await args.db.transaction().execute(async (trx) => { + const currentBranch = await trx + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + for (const { diffs, pluginKey, pluginDiffFunction } of pluginDiffs) { for (const diff of diffs ?? []) { // assume an insert or update operation as the default @@ -110,27 +139,17 @@ export async function handleFileChange(args: { // TODO: save hash of changed fles in every commit to discover inconsistent commits with blob? - const previousChanges = await trx - .selectFrom("change") - .selectAll() - .where("file_id", "=", fileId) - .where("plugin_key", "=", pluginKey) - .where("type", "=", diff.type) + const previousChange = await trx + .selectFrom("branch_change") + .leftJoin("change", "branch_change.change_id", "change.id") + .orderBy("branch_change.seq", "desc") + .where("change.file_id", "=", fileId) + .where("change.plugin_key", "=", pluginKey) + .where("change.type", "=", diff.type) + .where("branch_change.branch_id", "=", currentBranch.id) .where((eb) => eb.ref("value", "->>").key("id"), "=", value.id) - // TODO don't rely on created at. a plugin should report the parent id. - .execute(); - - // we need to finde the real leaf change as multiple changes can be set in the same created timestamp second or clockskew - let previousChange; // = previousChanges.at(-1); - for (let i = previousChanges.length - 1; i >= 0; i--) { - for (const change of previousChanges) { - if (change.parent_id === previousChanges[i]?.id) { - break; - } - } - previousChange = previousChanges[i]; - break; - } + .selectAll() + .executeTakeFirst(); // working change exists but is different from previously committed change // -> update the working change or delete if it is the same as previous uncommitted change @@ -141,30 +160,48 @@ export async function handleFileChange(args: { // working change exists but is identical to previously committed change if (previousChange) { previousCommittedDiff = await pluginDiffFunction?.[diff.type]?.({ - old: previousChange?.value, + old: previousChange.value, neu: diff.neu, }); - if (previousCommittedDiff?.length === 0) { - // drop the change because it's identical + if (previousCommittedDiff.length === 0) { continue; } } + const changeId = v4(); + await trx .insertInto("change") .values({ - id: v4(), + id: changeId, + value: value, + operation: diff.operation, + meta: diff.meta, type: diff.type, file_id: fileId, plugin_key: pluginKey, author: args.currentAuthor, - parent_id: previousChange?.id, - // @ts-expect-error - database expects stringified json - value: JSON.stringify(value), - // @ts-expect-error - database expects stringified json - meta: JSON.stringify(diff.meta), - operation: diff.operation, + parent_id: previousChange?.id || undefined, + }) + .execute(); + + const previousBranchChange = await trx + .selectFrom("branch_change") + .selectAll() + .where("branch_id", "=", currentBranch.id) + .orderBy("seq", "desc") + .executeTakeFirst(); + + const lastSeq = previousBranchChange?.seq ?? 0; + + await trx + .insertInto("branch_change") + .values({ + id: v4(), + seq: lastSeq + 1, + branch_id: currentBranch.id, + change_id: changeId, }) .execute(); } diff --git a/lix/packages/sdk/src/merge/merge.test.ts b/lix/packages/sdk/src/merge/merge.test.ts index 7351ee8fbb..caa8465a4f 100644 --- a/lix/packages/sdk/src/merge/merge.test.ts +++ b/lix/packages/sdk/src/merge/merge.test.ts @@ -3,7 +3,7 @@ import { test, expect, vi } from "vitest"; import { openLixInMemory } from "../open/openLixInMemory.js"; import { newLixFile } from "../newLix.js"; import { merge } from "./merge.js"; -import type { NewChange, NewCommit, NewConflict } from "../database/schema.js"; +import type { NewChange, NewConflict } from "../database/schema.js"; import type { LixPlugin } from "../plugin.js"; test("it should copy changes from the sourceLix into the targetLix that do not exist in targetLix yet", async () => { @@ -445,7 +445,6 @@ test("it should naively copy changes from the sourceLix into the targetLix that { id: "2", operation: "update", - commit_id: "commit-1", type: "mock", value: { id: "mock-id", color: "blue" }, file_id: "mock-file", @@ -453,13 +452,12 @@ test("it should naively copy changes from the sourceLix into the targetLix that }, ]; - const commitsOnlyInSourceLix: NewCommit[] = [ - { - id: "commit-1", - description: "", - parent_id: "0", - }, - ]; + // const commitsOnlyInSourceLix: NewCommit[] = [ + // { + // description: "", + // parent_id: "0", + // }, + // ]; const mockPlugin: LixPlugin = { key: "mock-plugin", @@ -491,17 +489,9 @@ test("it should naively copy changes from the sourceLix into the targetLix that .values(changesOnlyInSourceLix) .execute(); - await sourceLix.db - .insertInto("commit") - .values(commitsOnlyInSourceLix) - .execute(); - await merge({ sourceLix, targetLix }); const changes = await targetLix.db.selectFrom("change").selectAll().execute(); - const commits = await targetLix.db.selectFrom("commit").selectAll().execute(); expect(changes.length).toBe(1); - expect(commits.length).toBe(1); - expect(changes[0]?.commit_id).toBe("commit-1"); }); \ No newline at end of file diff --git a/lix/packages/sdk/src/merge/merge.ts b/lix/packages/sdk/src/merge/merge.ts index c4aa90ad2a..3a325867cd 100644 --- a/lix/packages/sdk/src/merge/merge.ts +++ b/lix/packages/sdk/src/merge/merge.ts @@ -8,25 +8,48 @@ import { getLeafChangesOnlyInSource } from "../query-utilities/get-leaf-changes- * Combined the changes of the source lix into the target lix. */ export async function merge(args: { - targetLix: Lix; sourceLix: Lix; + targetLix: Lix; + sourceBranchId?: string; + targetBranchId?: string; // TODO selectively merge changes // onlyTheseChanges }): Promise { // TODO increase performance by using attach mode // and only get the changes and commits that // are not in target. + + const sourceBranchId = + args.sourceBranchId || + ( + await args.sourceLix.db + .selectFrom("branch") + .select("id") + .where("name", "=", "main") + .executeTakeFirstOrThrow() + ).id; + + const targetBranchId = + args.targetBranchId || + ( + await args.targetLix.db + .selectFrom("branch") + .select("id") + .where("name", "=", "main") + .executeTakeFirstOrThrow() + ).id; + const sourceChanges = await args.sourceLix.db .selectFrom("change") + // .leftJoin("branch_change", "change.id", "branch_change.change_id") + // .where("branch_change.branch_id", "=", sourceBranchId) .selectAll() .execute(); - // TODO increase performance by only getting commits - // that are not in target in the future. - const sourceCommits = await args.sourceLix.db - .selectFrom("commit") - .selectAll() - .execute(); + // const sourceCommits = await args.sourceLix.db + // .selectFrom("commit") + // .selectAll() + // .execute(); // TODO don't query the changes again. inefficient. const leafChangesOnlyInSource = await getLeafChangesOnlyInSource({ @@ -103,15 +126,15 @@ export async function merge(args: { .execute(); } - // 2. copy the commits from source - if (sourceCommits.length > 0) { - await trx - .insertInto("commit") - .values(sourceCommits) - // ignore if already exists - .onConflict((oc) => oc.doNothing()) - .execute(); - } + // // 2. copy the commits from source + // if (sourceCommits.length > 0) { + // await trx + // .insertInto("commit") + // .values(sourceCommits) + // // ignore if already exists + // .onConflict((oc) => oc.doNothing()) + // .execute(); + // } // 3. insert the conflicts of those changes if (conflicts.length > 0) { diff --git a/lix/packages/sdk/src/newLix.test.ts b/lix/packages/sdk/src/newLix.test.ts index 6f0cbda3be..0f7c165863 100644 --- a/lix/packages/sdk/src/newLix.test.ts +++ b/lix/packages/sdk/src/newLix.test.ts @@ -9,7 +9,6 @@ test("inserting a change should auto fill the created_at column", async () => { .insertInto("change") .values({ id: "test", - commit_id: "test", type: "file", file_id: "mock", plugin_key: "mock-plugin", @@ -21,21 +20,3 @@ test("inserting a change should auto fill the created_at column", async () => { expect(changes).lengthOf(1); expect(changes[0]?.created_at).toBeDefined(); }); - -test("inserting a commit should auto fill the created_at column", async () => { - const lix = await openLixInMemory({ blob: await newLixFile() }); - - await lix.db - .insertInto("commit") - .values({ - id: "test", - parent_id: "test", - description: "test", - }) - .execute(); - - const commits = await lix.db.selectFrom("commit").selectAll().execute(); - - expect(commits).lengthOf(1); - expect(commits[0]?.created_at).toBeDefined(); -}); \ No newline at end of file diff --git a/lix/packages/sdk/src/open/openLix.ts b/lix/packages/sdk/src/open/openLix.ts index 4d78a0cb96..dc4a501b75 100644 --- a/lix/packages/sdk/src/open/openLix.ts +++ b/lix/packages/sdk/src/open/openLix.ts @@ -1,5 +1,4 @@ import type { LixPlugin } from "../plugin.js"; -import { commit } from "../commit.js"; import { handleFileChange, handleFileInsert } from "../file-handlers.js"; import { loadPlugins } from "../load-plugin.js"; import { contentFromDatabase, type SqliteDatabase } from "sqlite-wasm-kysely"; @@ -168,9 +167,6 @@ export async function openLix(args: { args.database.close(); await db.destroy(); }, - commit: (args: { description: string }) => { - return commit({ ...args, db, currentAuthor }); - }, }; } diff --git a/lix/packages/sdk/src/query-utilities/get-leaf-change.test.ts b/lix/packages/sdk/src/query-utilities/get-leaf-change.test.ts index 33a655ae71..a63f00c446 100644 --- a/lix/packages/sdk/src/query-utilities/get-leaf-change.test.ts +++ b/lix/packages/sdk/src/query-utilities/get-leaf-change.test.ts @@ -41,6 +41,33 @@ test("it should find the latest child of a given change", async () => { }, ]; + const currentBranch = await lix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + + await lix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentBranch.id, + change_id: "1", + seq: 1, + }, + { + branch_id: currentBranch.id, + change_id: "2", + seq: 2, + }, + { + branch_id: currentBranch.id, + change_id: "3", + seq: 3, + }, + ]) + .execute(); + await lix.db.insertInto("change").values(mockChanges).executeTakeFirst(); const leafOfChange01 = await getLeafChange({ diff --git a/lix/packages/sdk/src/query-utilities/get-leaf-change.ts b/lix/packages/sdk/src/query-utilities/get-leaf-change.ts index d1eacb34ad..4c4c6440f2 100644 --- a/lix/packages/sdk/src/query-utilities/get-leaf-change.ts +++ b/lix/packages/sdk/src/query-utilities/get-leaf-change.ts @@ -10,19 +10,38 @@ export async function getLeafChange(args: { const _true = true; let nextChange = args.change; + const currentBranch = await args.lix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", _true) + .executeTakeFirstOrThrow(); while (_true) { const childChange = await args.lix.db - .selectFrom("change") + .selectFrom("branch_change") + .fullJoin("change", "branch_change.change_id", "change.id") .selectAll() - .where("parent_id", "=", nextChange.id) + .select([ + "author", + "change.id as id", + "change.parent_id as parent_id", + "type", + "file_id", + "plugin_key", + "operation", + "value", + "meta", + "created_at", + ]) + .where("branch_id", "=", currentBranch.id) + .where("change.parent_id", "=", nextChange?.id) .executeTakeFirst(); if (!childChange) { break; } - nextChange = childChange; + nextChange = childChange as Change; } return nextChange; diff --git a/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.test.ts b/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.test.ts index 66f4529510..faa9ae2f8a 100644 --- a/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.test.ts +++ b/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.test.ts @@ -7,9 +7,19 @@ test("it should find the common parent of two changes recursively", async () => const sourceLix = await openLixInMemory({ blob: await newLixFile(), }); + const currentSourceBranch = await sourceLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); const targetLix = await openLixInMemory({ blob: await newLixFile(), }); + const currentTargetBranch = await targetLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); const mockChanges: Change[] = [ { @@ -49,12 +59,44 @@ test("it should find the common parent of two changes recursively", async () => .values([mockChanges[0]!]) .executeTakeFirst(); + await targetLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentTargetBranch.id, + change_id: "0", + seq: 1, + }, + ]) + .execute(); + await sourceLix.db .insertInto("change") // lix b has two update changes .values([mockChanges[0]!, mockChanges[1]!, mockChanges[2]!]) .executeTakeFirst(); + await sourceLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentSourceBranch.id, + change_id: "0", + seq: 1, + }, + { + branch_id: currentSourceBranch.id, + change_id: "1", + seq: 2, + }, + { + branch_id: currentSourceBranch.id, + change_id: "2", + seq: 3, + }, + ]) + .execute(); + const commonParent = await getLowestCommonAncestor({ sourceChange: mockChanges[2]!, sourceLix, @@ -128,9 +170,20 @@ test("it should return the source change if its the common parent", async () => const sourceLix = await openLixInMemory({ blob: await newLixFile(), }); + const currentSourceBranch = await sourceLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); + const targetLix = await openLixInMemory({ blob: await newLixFile(), }); + const currentTargetBranch = await targetLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); const mockChanges: Change[] = [ { @@ -160,11 +213,43 @@ test("it should return the source change if its the common parent", async () => .values([mockChanges[0]!, mockChanges[1]!]) .executeTakeFirst(); + await targetLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentTargetBranch.id, + change_id: "0", + seq: 1, + }, + { + branch_id: currentTargetBranch.id, + change_id: "1", + seq: 2, + }, + ]) + .execute(); + await sourceLix.db .insertInto("change") .values([mockChanges[0]!, mockChanges[1]!]) .executeTakeFirst(); + await sourceLix.db + .insertInto("branch_change") + .values([ + { + branch_id: currentSourceBranch.id, + change_id: "0", + seq: 1, + }, + { + branch_id: currentSourceBranch.id, + change_id: "1", + seq: 2, + }, + ]) + .execute(); + const commonParent = await getLowestCommonAncestor({ sourceChange: mockChanges[1]!, targetLix, diff --git a/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.ts b/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.ts index 7090eb0052..9fd3abb4f1 100644 --- a/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.ts +++ b/lix/packages/sdk/src/query-utilities/get-lowest-common-ancestor.ts @@ -15,25 +15,68 @@ export async function getLowestCommonAncestor(args: { if (args.sourceChange?.parent_id === undefined) { return undefined; } + const currentTargetBranch = await args.targetLix.db + .selectFrom("branch") + .selectAll() + .where("active", "=", true) + .executeTakeFirstOrThrow(); - const changeExistsInTarget = await args.targetLix.db - .selectFrom("change") + const currentSourceBranch = await args.sourceLix.db + .selectFrom("branch") .selectAll() - .where("id", "=", args.sourceChange.id) + .where("active", "=", true) + .executeTakeFirstOrThrow(); + + const changeExistsInTarget = await args.targetLix.db + .selectFrom("branch_change") + .leftJoin("change", "branch_change.change_id", "change.id") + .select([ + "author", + "change.id as id", + "change.parent_id as parent_id", + "type", + "file_id", + "plugin_key", + "operation", + "value", + "meta", + "created_at", + ]) + .orderBy("seq", "desc") + .where("branch_id", "=", currentTargetBranch.id) + .where("change.id", "=", args.sourceChange.id) .executeTakeFirst(); if (changeExistsInTarget) { - return changeExistsInTarget; + return changeExistsInTarget as Change; } let nextChange: Change | undefined; const _true = true; while (_true) { - nextChange = await args.sourceLix.db - .selectFrom("change") - .selectAll() - .where("id", "=", nextChange?.parent_id ?? args.sourceChange.parent_id) - .executeTakeFirst(); + nextChange = (await args.sourceLix.db + .selectFrom("branch_change") + .leftJoin("change", "branch_change.change_id", "change.id") + .select([ + "author", + "change.id as id", + "change.parent_id as parent_id", + "type", + "file_id", + "plugin_key", + "operation", + "value", + "meta", + "created_at", + ]) + .where("branch_id", "=", currentSourceBranch.id) + .orderBy("seq", "desc") + .where( + "change.id", + "=", + nextChange?.parent_id ?? args.sourceChange.parent_id, + ) + .executeTakeFirst()) as Change; if (!nextChange) { // end of the change sequence. No common parent found. @@ -41,13 +84,26 @@ export async function getLowestCommonAncestor(args: { } const changeExistsInTarget = await args.targetLix.db - .selectFrom("change") - .selectAll() - .where("id", "=", nextChange.id) + .selectFrom("branch_change") + .leftJoin("change", "branch_change.change_id", "change.id") + .select([ + "author", + "change.id as id", + "change.parent_id as parent_id", + "type", + "file_id", + "plugin_key", + "operation", + "value", + "meta", + "created_at", + ]) + .where("branch_id", "=", currentTargetBranch.id) + .where("change.id", "=", nextChange.id) .executeTakeFirst(); if (changeExistsInTarget) { - return changeExistsInTarget; + return changeExistsInTarget as Change; } } return; diff --git a/lix/packages/sdk/src/query-utilities/index.ts b/lix/packages/sdk/src/query-utilities/index.ts index a0678eea2b..d853132b77 100644 --- a/lix/packages/sdk/src/query-utilities/index.ts +++ b/lix/packages/sdk/src/query-utilities/index.ts @@ -1,4 +1,3 @@ export { getLeafChange } from "./get-leaf-change.js"; export { getLowestCommonAncestor } from "./get-lowest-common-ancestor.js"; export { getLeafChangesOnlyInSource } from "./get-leaf-changes-only-in-source.js"; -export { isInSimulatedCurrentBranch } from "./is-in-simulated-branch.js"; \ No newline at end of file diff --git a/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.test.ts b/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.test.ts deleted file mode 100644 index 79e30606f0..0000000000 --- a/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* eslint-disable unicorn/no-null */ -import { test, expect } from "vitest"; -import { openLixInMemory } from "../open/openLixInMemory.js"; -import { newLixFile } from "../newLix.js"; -import { isInSimulatedCurrentBranch } from "./is-in-simulated-branch.js"; - -test("as long as a conflict is unresolved, the conflicting change should not appear in the current branch", async () => { - const lix = await openLixInMemory({ blob: await newLixFile() }); - - const changes = await lix.db - .insertInto("change") - .values([ - { - id: "change0", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change1", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change2", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - ]) - .returningAll() - .execute(); - - await lix.db - .insertInto("conflict") - .values([ - { - change_id: "change0", - conflicting_change_id: "change2", - resolved_with_change_id: null, - }, - ]) - .returningAll() - .execute(); - - const changesInCurrentBranch = await lix.db - .selectFrom("change") - .selectAll() - .where(isInSimulatedCurrentBranch) - .execute(); - - expect(changesInCurrentBranch.map((c) => c.id)).toEqual([ - changes[0]?.id, - changes[1]?.id, - ]); -}); - -test(`if the conflict has been resolved by selecting the 'original' change, - the 'conflicting' change should not be in the current branch`, async () => { - const lix = await openLixInMemory({ blob: await newLixFile() }); - - const changes = await lix.db - .insertInto("change") - .values([ - { - id: "change0", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change1", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change2", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - ]) - .returningAll() - .execute(); - - await lix.db - .insertInto("conflict") - .values([ - { - change_id: "change0", - conflicting_change_id: "change2", - resolved_with_change_id: "change0", - }, - ]) - .returningAll() - .execute(); - - const changesInCurrentBranch = await lix.db - .selectFrom("change") - .selectAll() - .where(isInSimulatedCurrentBranch) - .execute(); - - expect(changesInCurrentBranch.map((c) => c.id)).toEqual([ - changes[0]?.id, - changes[1]?.id, - ]); -}); - -test(` - if the conflict has been resolved by selecting the conflicting change, - and rejecting the original change, the conflicting change should appear - in the branch while the original change is excluded`, async () => { - const lix = await openLixInMemory({ blob: await newLixFile() }); - - const changes = await lix.db - .insertInto("change") - .values([ - { - id: "change0", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change1", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - { - id: "change2", - file_id: "mock", - operation: "create", - plugin_key: "mock", - type: "mock", - }, - ]) - .returningAll() - .execute(); - - await lix.db - .insertInto("conflict") - .values([ - { - change_id: "change0", - conflicting_change_id: "change2", - resolved_with_change_id: "change2", - }, - ]) - .returningAll() - .execute(); - - const changesInCurrentBranch = await lix.db - .selectFrom("change") - .selectAll() - .where(isInSimulatedCurrentBranch) - .execute(); - - expect(changesInCurrentBranch.map((c) => c.id)).toEqual([ - changes[1]?.id, - changes[2]?.id, - ]); -}); diff --git a/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.ts b/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.ts deleted file mode 100644 index ddf134b223..0000000000 --- a/lix/packages/sdk/src/query-utilities/is-in-simulated-branch.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable unicorn/no-null */ -import type { ExpressionBuilder } from "kysely"; -import type { LixDatabaseSchema } from "../database/schema.js"; - -/** - * This is a simulated current branch until the - * branching concept is implemented https://linear.app/opral/issue/LIX-126/branching. - * - * Feel free to use your own simulated branch implementation. - * - * @example - * const changesInCurrentBranch = await lix.db - * .selectFrom("change") - * .selectAll() - * .where(isInSimulatedBranch) - * .execute(); - */ -export function isInSimulatedCurrentBranch( - eb: ExpressionBuilder, -) { - return eb.or([ - // change is not in a conflict - eb("change.id", "not in", (subquery) => - subquery.selectFrom("conflict").select("conflict.change_id").unionAll( - // @ts-expect-error - no idea why - subquery - .selectFrom("conflict") - .select("conflict.conflicting_change_id"), - ), - ), - // change is in a conflict that has not been resolved - // AND the change is not the conflicting one - eb("change.id", "in", (subquery) => - subquery - .selectFrom("conflict") - .select("conflict.change_id") - .where("conflict.resolved_with_change_id", "is", null), - ), - // change is in a conflict and is the resolved one - // @ts-expect-error - no idea why - eb("change.id", "in", (subquery) => - subquery - .selectFrom("conflict") - .select("conflict.resolved_with_change_id"), - ), - ]); -}