Skip to content

Commit

Permalink
Merge pull request #3186 from opral/lixdk-161-introduce-concept-of-ch…
Browse files Browse the repository at this point in the history
…ange-sets

add: change sets
  • Loading branch information
samuelstroschein authored Oct 25, 2024
2 parents 4876783 + 0beaeef commit 3f573dc
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 7 deletions.
43 changes: 43 additions & 0 deletions packages/lix-sdk/src/change-set/create-change-set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { test, expect } from "vitest";
import { openLixInMemory } from "../open/openLixInMemory.js";
import { createChangeSet } from "./create-change-set.js";

test("creating a change set should succeed", async () => {
const lix = await openLixInMemory({});

const mockChanges = await lix.db
.insertInto("change")
.values([
{
type: "file",
entity_id: "value1",
file_id: "mock",
plugin_key: "mock-plugin",
snapshot_id: "sn1",
},
{
type: "file",
entity_id: "value2",
file_id: "mock",
plugin_key: "mock-plugin",
snapshot_id: "sn2",
},
])
.returningAll()
.execute();

const changeSet = await createChangeSet({
lix,
changeIds: mockChanges.map((change) => change.id),
});

const changeSetMembers = await lix.db
.selectFrom("change_set_item")
.selectAll()
.where("change_set_id", "=", changeSet.id)
.execute();

expect(changeSetMembers.map((member) => member.change_id)).toEqual(
expect.arrayContaining([mockChanges[0]?.id, mockChanges[1]?.id]),
);
});
26 changes: 26 additions & 0 deletions packages/lix-sdk/src/change-set/create-change-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ChangeSet } from "../database/schema.js";
import type { Lix } from "../types.js";

export async function createChangeSet(args: {
lix: Lix;
changeIds: string[];
}): Promise<ChangeSet> {
return await args.lix.db.transaction().execute(async (trx) => {
const changeSet = await trx
.insertInto("change_set")
.defaultValues()
.returningAll()
.executeTakeFirstOrThrow();

for (const changeId of args.changeIds) {
await trx
.insertInto("change_set_item")
.values({
change_id: changeId,
change_set_id: changeSet.id,
})
.execute();
}
return changeSet;
});
}
19 changes: 17 additions & 2 deletions packages/lix-sdk/src/database/applySchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export async function applySchema(args: { sqlite: SqliteDatabase }) {
content TEXT
) strict;
-- conflicts
CREATE INDEX IF NOT EXISTS idx_content_hash ON snapshot (id);
CREATE TABLE IF NOT EXISTS conflict (
Expand Down Expand Up @@ -79,11 +81,25 @@ export async function applySchema(args: { sqlite: SqliteDatabase }) {
insert or replace into file_internal(id, path, data, metadata) values(OLD.file_id, OLD.path, OLD.data, OLD.metadata);
END;
-- change sets
CREATE TABLE IF NOT EXISTS change_set (
id TEXT PRIMARY KEY DEFAULT (uuid_v4())
) strict;
CREATE TABLE IF NOT EXISTS change_set_item (
change_set_id TEXT NOT NULL,
change_id TEXT NOT NULL,
UNIQUE(change_set_id, change_id),
FOREIGN KEY(change_set_id) REFERENCES change_set(id),
FOREIGN KEY(change_id) REFERENCES change(id)
) strict;
-- change discussions
CREATE TABLE IF NOT EXISTS discussion (
-- TODO https://github.com/opral/lix-sdk/issues/74 replace with uuid_v7
id TEXT PRIMARY KEY DEFAULT (uuid_v4())
) strict;
Expand All @@ -99,7 +115,6 @@ export async function applySchema(args: { sqlite: SqliteDatabase }) {
CREATE TABLE IF NOT EXISTS comment (
--- TODO in inlang i saw we replace uuid_v3 with uuid_v7 any reason we use v4 in lix?
id TEXT PRIMARY KEY DEFAULT (uuid_v4()),
parent_id TEXT,
discussion_id TEXT NULL,
Expand Down
34 changes: 34 additions & 0 deletions packages/lix-sdk/src/database/initDb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,37 @@ test("change edges can't reference themselves", async () => {
`[SQLite3Error: SQLITE_CONSTRAINT_CHECK: sqlite3 result code 275: CHECK constraint failed: parent_id != child_id]`,
);
});

test("change set items must be unique", async () => {
const sqlite = await createInMemoryDatabase({
readOnly: false,
});
const db = initDb({ sqlite });

await db
.insertInto("change_set")
.defaultValues()
.returningAll()
.executeTakeFirstOrThrow();

await db
.insertInto("change_set_item")
.values({
change_set_id: "change-set-1",
change_id: "change-1",
})
.execute();

await expect(
db
.insertInto("change_set_item")
.values({
change_set_id: "change-set-1",
change_id: "change-1",
})
.returningAll()
.execute(),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[SQLite3Error: SQLITE_CONSTRAINT_UNIQUE: sqlite3 result code 2067: UNIQUE constraint failed: change_set_item.change_set_id, change_set_item.change_id]`,
);
});
21 changes: 21 additions & 0 deletions packages/lix-sdk/src/database/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export type LixDatabaseSchema = {
change_edge: ChangeEdgeTable;
conflict: ConflictTable;
snapshot: SnapshotTable;
// change set
change_set: ChangeSetTable;
change_set_item: ChangeSetItem;

// discussion
discussion: DiscussionTable;
Expand Down Expand Up @@ -116,6 +119,24 @@ type ConflictTable = {
resolved_change_id: string | null;
};

// ------ change sets ------

export type ChangeSet = Selectable<ChangeSetTable>;
export type NewChangeSet = Insertable<ChangeSetTable>;
export type ChangeSetUpdate = Updateable<ChangeSetTable>;
type ChangeSetTable = {
id: Generated<string>;
};

export type ChangeSetItem = Selectable<ChangeSetItemTable>;
export type NewChangeSetItem = Insertable<ChangeSetItemTable>;
export type ChangeSetItemUpdate = Updateable<ChangeSetItemTable>;
type ChangeSetItemTable = {
change_set_id: string;
change_id: string;
};


// ------ discussions ------

export type Discussion = Selectable<DiscussionTable>;
Expand Down
1 change: 1 addition & 0 deletions packages/lix-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "./types.js";
export * from "./query-utilities/index.js";
export * from "./resolve-conflict/errors.js";
export { merge } from "./merge/merge.js";
export { createChangeSet } from "./change-set/create-change-set.js";

// TODO maybe move to `lix.*` api
// https://github.com/opral/lix-sdk/issues/58
Expand Down
82 changes: 82 additions & 0 deletions packages/lix-sdk/src/merge/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from "../database/schema.js";
import type { LixPlugin } from "../plugin.js";
import { mockJsonSnapshot } from "../query-utilities/mock-json-snapshot.js";
import { createChangeSet } from "../change-set/create-change-set.js";

test("it should copy changes from the sourceLix into the targetLix that do not exist in targetLix yet", async () => {
const mockSnapshots = [
Expand Down Expand Up @@ -735,3 +736,84 @@ test("it should copy discussion and related comments and mappings", async () =>

// TODO add test for discussions and discussion maps
});

test("it should copy change sets and merge memberships", async () => {
const targetLix = await openLixInMemory({});

const mockChanges = await targetLix.db
.insertInto("change")
.values([
{
type: "file",
entity_id: "value1",
file_id: "mock",
plugin_key: "mock-plugin",
snapshot_id: "sn1",
},
{
type: "file",
entity_id: "value2",
file_id: "mock",
plugin_key: "mock-plugin",
snapshot_id: "sn2",
},
])
.returningAll()
.execute();

const changeSet1 = await createChangeSet({
lix: targetLix,
changeIds: [mockChanges[0]!.id],
});

const sourceLix = await openLixInMemory({
blob: await targetLix.toBlob(),
});

// expand the change set to contain another change
// to test if the sets are merged
await targetLix.db
.insertInto("change_set_item")
.values({
change_set_id: changeSet1.id,
change_id: mockChanges[1]!.id,
})
.execute();

// create a new set just for change [1]
const changeSet2 = await createChangeSet({
lix: targetLix,
changeIds: [mockChanges[1]!.id],
});

await merge({ sourceLix, targetLix });

const changeSets = await targetLix.db
.selectFrom("change_set")
.selectAll()
.execute();

const changeSet1Items = await targetLix.db
.selectFrom("change_set_item")
.selectAll()
.where("change_set_id", "=", changeSet1.id)
.execute();
const changeSet2Items = await targetLix.db
.selectFrom("change_set_item")
.selectAll()
.where("change_set_id", "=", changeSet2.id)
.execute();

// expect two change sets
expect(changeSets.length).toBe(2);

// expect merger of the change set to contain both changes
expect(changeSet1Items.map((item) => item.change_id)).toEqual(
expect.arrayContaining([mockChanges[0]?.id, mockChanges[1]?.id]),
);

// expect the second change set to contain only the second change
expect(changeSet2Items.map((item) => item.change_id)).toEqual(
expect.arrayContaining([mockChanges[1]?.id]),
);
});
41 changes: 36 additions & 5 deletions packages/lix-sdk/src/merge/merge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { LixPlugin } from "../plugin.js";
import type { Lix } from "../types.js";
import { getLeafChangesOnlyInSource } from "../query-utilities/get-leaf-changes-only-in-source.js";

Expand Down Expand Up @@ -28,15 +27,15 @@ export async function merge(args: {

// 2. Let the plugin detect conflicts

const plugin = args.sourceLix.plugins[0] as LixPlugin;
const plugin = args.sourceLix.plugins[0];

// TODO function assumes that all changes belong to the same file
if (args.sourceLix.plugins.length !== 1) {
if (args.sourceLix.plugins.length > 1) {
throw new Error("Unimplemented. Only one plugin is supported for now");
}

const conflicts =
(await plugin.detectConflicts?.({
(await plugin?.detectConflicts?.({
sourceLix: args.sourceLix,
targetLix: args.targetLix,
})) ?? [];
Expand Down Expand Up @@ -80,7 +79,7 @@ export async function merge(args: {
.executeTakeFirst();
}

if (!plugin.applyChanges) {
if (!plugin?.applyChanges) {
throw new Error("Plugin does not support applying changes");
}

Expand Down Expand Up @@ -116,11 +115,25 @@ export async function merge(args: {
.selectAll()
.execute();

// change graph

const sourceEdges = await args.sourceLix.db
.selectFrom("change_edge")
.selectAll()
.execute();

// change sets

const sourceChangeSets = await args.sourceLix.db
.selectFrom("change_set")
.selectAll()
.execute();

const sourceChangeSetItems = await args.sourceLix.db
.selectFrom("change_set_item")
.selectAll()
.execute();

await args.targetLix.db.transaction().execute(async (trx) => {
if (sourceChangesWithSnapshot.length > 0) {
// copy the snapshots from source
Expand Down Expand Up @@ -178,6 +191,24 @@ export async function merge(args: {
.execute();
}

// add change sets and change_set_memberships
if (sourceChangeSets.length > 0) {
await trx
.insertInto("change_set")
.values(sourceChangeSets)
// ignore if already exists
.onConflict((oc) => oc.doNothing())
.execute();
}
if (sourceChangeSetItems.length > 0) {
await trx
.insertInto("change_set_item")
.values(sourceChangeSetItems)
// ignore if already exists
.onConflict((oc) => oc.doNothing())
.execute();
}

// add discussions, comments and discsussion_change_mappings

if (sourceDiscussions.length > 0) {
Expand Down

0 comments on commit 3f573dc

Please sign in to comment.