Skip to content

Commit

Permalink
Merge pull request #3150 from opral/mesdk-252-fix-foreign-key-constra…
Browse files Browse the repository at this point in the history
…ints

fix: re-enable foreign keys
  • Loading branch information
samuelstroschein authored Sep 25, 2024
2 parents 9ad32d1 + ab8cb66 commit 588e066
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 71 deletions.
27 changes: 13 additions & 14 deletions inlang/source-code/sdk2/src/database/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import {
} from "../json-schema/pattern.js";

export function applySchema(args: { sqlite: SqliteDatabase }) {
// TODO re-enable https://github.com/opral/inlang-sdk/issues/209
// const foreignKeyActivated: any = args.sqlite.exec("PRAGMA foreign_keys", {
// returnValue: "resultRows",
// });
// if (
// // first row that is returned
// // first column of the first row
// // is equal to 0, then foreign keys are disabled
// foreignKeyActivated[0][0] === 0
// ) {
// args.sqlite.exec("PRAGMA foreign_keys = ON", {
// returnValue: "resultRows",
// });
// }
const foreignKeyActivated: any = args.sqlite.exec("PRAGMA foreign_keys", {
returnValue: "resultRows",
});
if (
// first row that is returned
// first column of the first row
// is equal to 0, then foreign keys are disabled
foreignKeyActivated[0][0] === 0
) {
args.sqlite.exec("PRAGMA foreign_keys = ON", {
returnValue: "resultRows",
});
}

args.sqlite.exec(`
CREATE TABLE IF NOT EXISTS bundle (
Expand Down
102 changes: 96 additions & 6 deletions inlang/source-code/sdk2/src/lix-plugin/applyChanges.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { getLeafChange, type LixPlugin } from "@lix-js/sdk";
import {
getLeafChange,
type Change,
type LixPlugin,
type LixReadonly,
} from "@lix-js/sdk";
import { contentFromDatabase, loadDatabaseInMemory } from "sqlite-wasm-kysely";
import { initDb } from "../database/initDb.js";
import type { Kysely } from "kysely";
import type { InlangDatabaseSchema } from "../database/schema.js";

export const applyChanges: NonNullable<LixPlugin["applyChanges"]> = async ({
lix,
Expand Down Expand Up @@ -68,11 +75,94 @@ export const applyChanges: NonNullable<LixPlugin["applyChanges"]> = async ({

// upsert the value
const value = leafChange.value as any;
await db
.insertInto(leafChange.type as "bundle" | "message" | "variant")
.values(value)
.onConflict((c) => c.column("id").doUpdateSet(value))
.execute();

try {
await db
.insertInto(leafChange.type as "bundle" | "message" | "variant")
.values(value)
.onConflict((c) => c.column("id").doUpdateSet(value))
.execute();
} catch (e) {
// 787 = SQLITE_CONSTRAINT_FOREIGNKEY
if (e instanceof Error && (e as any)?.resultCode === 787) {
await handleForeignKeyViolation({ change: leafChange, lix, db });
} else {
throw e;
}
}
}
return { fileData: contentFromDatabase(sqlite) };
};

/**
* Handles foreign key violations e.g. a change
* doesn't exist in the target database but is referenced
* by an entity.
*/
async function handleForeignKeyViolation(args: {
change: Change;
lix: LixReadonly;
db: Kysely<InlangDatabaseSchema>;
}) {
const lastKnown = async (
type: "bundle" | "message" | "variant",
id: string
) =>
await args.lix.db
.selectFrom("change")
.selectAll()
// heuristic that getting the last bundle value is fine
// and using created_at is fine too. if the change is undesired
// , a user can revert it with lix change control
.orderBy("created_at desc")
.where("type", "=", type)
.where((eb) => eb.ref("value", "->>").key("id"), "=", id)
.where("operation", "in", ["create", "update"])
// TODO shouldn't throw. The API needs to be able to
// report issues back to the app without throwing and potentially failing
// to apply 1000 changes because 1 change is invalid
// same requirement as in inlang, see https://github.com/opral/inlang-sdk/issues/213
.executeTakeFirstOrThrow();

if (args.change.type === "message") {
const lastKnownBundle = await lastKnown(
"bundle",
args.change.value?.bundleId
);
await args.db
.insertInto("bundle")
.values(lastKnownBundle.value as any)
.execute();
} else if (args.change.type === "variant") {
const lastKnownMessage = await lastKnown(
"message",
args.change.value?.messageId
);
// getting the bundle too out of precaution
const lastKnownBundle = await lastKnown(
"bundle",
lastKnownMessage.value?.bundleId
);
await args.db
.insertInto("bundle")
.values(lastKnownBundle.value as any)
// the bundle exists, so we can ignore the conflict
.onConflict((c) => c.doNothing())
.execute();
await args.db
.insertInto("message")
.values(lastKnownMessage.value as any)
.execute();
await args.db
.insertInto(args.change.type as "bundle" | "message" | "variant")
.values(args.change.value as any)
.onConflict((c) => c.column("id").doUpdateSet(args.change.value as any))
.execute();
}
// re-execute applying the change
await args.db
.insertInto(args.change.type as "bundle" | "message" | "variant")
.values(args.change.value as any)
.onConflict((c) => c.column("id").doUpdateSet(args.change.value as any))
.execute();
}
138 changes: 91 additions & 47 deletions inlang/source-code/sdk2/src/lix-plugin/inlangLixPluginV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ describe("plugin.diff.file", () => {

test("insert of message", async () => {
const neuProject = await loadProjectInMemory({ blob: await newProject() });
await neuProject.db
.insertInto("bundle")
.values({ id: "unknown" })
.execute();
await neuProject.db
.insertInto("message")
.values({
Expand All @@ -110,22 +114,29 @@ describe("plugin.diff.file", () => {
metadata: {},
},
});
expect(diffReports).toEqual([
{
type: "message",
operation: "create",
old: undefined,
neu: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "en",
},
} satisfies DiffReport,
]);
expect(diffReports).toEqual(
expect.arrayContaining([
{
type: "message",
operation: "create",
old: undefined,
neu: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "en",
},
} satisfies DiffReport,
])
);
});

test("update of message", async () => {
const oldProject = await loadProjectInMemory({ blob: await newProject() });
await oldProject.db
.insertInto("bundle")
.values({ id: "unknown" })
.execute();
await oldProject.db
.insertInto("message")
.values([
Expand All @@ -142,6 +153,10 @@ describe("plugin.diff.file", () => {
])
.execute();
const neuProject = await loadProjectInMemory({ blob: await newProject() });
await neuProject.db
.insertInto("bundle")
.values({ id: "unknown" })
.execute();
await neuProject.db
.insertInto("message")
.values([
Expand Down Expand Up @@ -171,30 +186,41 @@ describe("plugin.diff.file", () => {
metadata: {},
},
});
expect(diffReports).toEqual([
{
meta: {
id: "1",
},
type: "message",
operation: "update",
old: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "en",
},
neu: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "de",
},
} satisfies DiffReport,
]);
expect(diffReports).toEqual(
expect.arrayContaining([
{
meta: {
id: "1",
},
type: "message",
operation: "update",
old: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "en",
},
neu: {
id: "1",
bundleId: "unknown",
selectors: [],
locale: "de",
},
} satisfies DiffReport,
])
);
});
test("insert of variant", async () => {
const neuProject = await loadProjectInMemory({ blob: await newProject() });
await neuProject.db
.insertInto("bundle")
.values({ id: "bundle1" })
.execute();
await neuProject.db
.insertInto("message")
.values({ id: "1", bundleId: "bundle1", locale: "en" })
.execute();

await neuProject.db
.insertInto("variant")
.values({
Expand All @@ -213,22 +239,32 @@ describe("plugin.diff.file", () => {
metadata: {},
},
});
expect(diffReports).toEqual([
{
type: "variant",
operation: "create",
old: undefined,
neu: {
id: "1",
messageId: "1",
pattern: [{ type: "text", value: "hello world" }],
matches: [],
},
} satisfies DiffReport,
]);
expect(diffReports).toEqual(
expect.arrayContaining([
{
type: "variant",
operation: "create",
old: undefined,
neu: {
id: "1",
messageId: "1",
pattern: [{ type: "text", value: "hello world" }],
matches: [],
},
} satisfies DiffReport,
])
);
});
test("update of variant", async () => {
const oldProject = await loadProjectInMemory({ blob: await newProject() });
await oldProject.db
.insertInto("bundle")
.values({ id: "bundle1" })
.execute();
await oldProject.db
.insertInto("message")
.values({ id: "1", bundleId: "bundle1", locale: "en" })
.execute();
await oldProject.db
.insertInto("variant")
.values([
Expand All @@ -247,6 +283,14 @@ describe("plugin.diff.file", () => {
])
.execute();
const neuProject = await loadProjectInMemory({ blob: await newProject() });
await neuProject.db
.insertInto("bundle")
.values({ id: "bundle1" })
.execute();
await neuProject.db
.insertInto("message")
.values({ id: "1", bundleId: "bundle1", locale: "en" })
.execute();
await neuProject.db
.insertInto("variant")
.values([
Expand Down
Loading

0 comments on commit 588e066

Please sign in to comment.