mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
fix(cli): keep session database compatible with released clients (#12207)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep shared session databases writable by released Kilo clients after newer schema migrations run.
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
DELETE FROM `session_message`;--> statement-breakpoint
|
||||
ALTER TABLE `session_message` ADD `seq` integer NOT NULL;--> statement-breakpoint
|
||||
-- kilocode_change
|
||||
ALTER TABLE `session_message` ADD `seq` integer;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint
|
||||
CREATE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint
|
||||
|
||||
+1
-1
@@ -756,7 +756,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
|
||||
@@ -830,7 +830,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
|
||||
+1
-1
@@ -874,7 +874,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
|
||||
+1
-1
@@ -874,7 +874,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
|
||||
+1
-1
@@ -938,7 +938,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
|
||||
+2
-2
@@ -950,7 +950,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
@@ -2080,4 +2080,4 @@
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- kilocode_change - new file
|
||||
CREATE TABLE `__new_session_message` (
|
||||
`id` text PRIMARY KEY,
|
||||
`session_id` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`seq` integer,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_session_message`(`id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data`) SELECT `id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data` FROM `session_message`;--> statement-breakpoint
|
||||
DROP TABLE `session_message`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_session_message` RENAME TO `session_message`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint
|
||||
CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`);--> statement-breakpoint
|
||||
CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);
|
||||
+2083
File diff suppressed because it is too large
Load Diff
+1
@@ -34,5 +34,6 @@ export const migrations = (
|
||||
import("./migration/20260604172448_event_sourced_session_input"),
|
||||
import("./migration/20260605003541_add_session_context_snapshot"),
|
||||
import("./migration/20260605042240_add_context_epoch_agent"),
|
||||
import("./migration/20260714141136_session-message-legacy-writer-compat"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ export default {
|
||||
// Pre-launch Session projections were written before durable event persistence
|
||||
// became unconditional, so they cannot be assigned truthful aggregate order.
|
||||
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer;`) // kilocode_change
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
||||
yield* tx.run(
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// kilocode_change - new file
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260714141136_session-message-legacy-writer-compat",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`__new_session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`seq\` integer,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`INSERT INTO \`__new_session_message\`(\`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\`) SELECT \`id\`, \`session_id\`, \`type\`, \`seq\`, \`time_created\`, \`time_updated\`, \`data\` FROM \`session_message\`;`)
|
||||
yield* tx.run(`DROP TABLE \`session_message\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`__new_session_message\` RENAME TO \`session_message\`;`)
|
||||
yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -2,7 +2,7 @@ export * as SessionV2 from "./session"
|
||||
export * from "./session/schema"
|
||||
|
||||
import { Cause, Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, gt, isNotNull, like, lt, or, type SQL } from "drizzle-orm" // kilocode_change
|
||||
import { ProjectV2 } from "./project"
|
||||
import { WorkspaceV2 } from "./workspace"
|
||||
import { ModelV2 } from "./model"
|
||||
@@ -304,20 +304,25 @@ export const layer = Layer.effect(
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)),
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, input.sessionID),
|
||||
eq(SessionMessageTable.id, input.cursor.id),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
: undefined
|
||||
if (input.cursor && !anchor) return []
|
||||
const boundary = anchor
|
||||
const seq = anchor?.seq
|
||||
if (input.cursor && seq == null) return []
|
||||
const boundary = seq != null
|
||||
? order === "asc"
|
||||
? gt(SessionMessageTable.seq, anchor.seq)
|
||||
: lt(SessionMessageTable.seq, anchor.seq)
|
||||
? gt(SessionMessageTable.seq, seq)
|
||||
: lt(SessionMessageTable.seq, seq)
|
||||
: undefined
|
||||
const where = boundary
|
||||
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
||||
: eq(SessionMessageTable.session_id, input.sessionID)
|
||||
? and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq), boundary)
|
||||
: and(eq(SessionMessageTable.session_id, input.sessionID), isNotNull(SessionMessageTable.seq)) // kilocode_change
|
||||
const query = db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, asc, desc, eq, gt, gte, ne, or } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, gt, gte, isNotNull, ne, or } from "drizzle-orm" // kilocode_change
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { MessageDecodeError } from "./error"
|
||||
@@ -11,14 +11,22 @@ type DatabaseService = Database.Interface["db"]
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
|
||||
const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
return yield* db
|
||||
const row = yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
eq(SessionMessageTable.type, "compaction"),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row || row.seq === null) return
|
||||
return { seq: row.seq }
|
||||
})
|
||||
|
||||
const messageRows = Effect.fnUntraced(function* (
|
||||
@@ -33,6 +41,7 @@ const messageRows = Effect.fnUntraced(function* (
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
compaction
|
||||
? or(
|
||||
gte(SessionMessageTable.seq, compaction.seq),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionProjector from "./projector"
|
||||
|
||||
import { and, desc, eq, sql } from "drizzle-orm"
|
||||
import { and, desc, eq, isNotNull, sql } from "drizzle-orm" // kilocode_change
|
||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
@@ -138,7 +138,11 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "assistant")),
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "assistant"),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
.limit(1)
|
||||
@@ -159,6 +163,7 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
||||
eq(SessionMessageTable.id, messageID),
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "assistant"),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.get()
|
||||
@@ -174,7 +179,11 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "compaction")),
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "compaction"),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
.limit(1)
|
||||
@@ -190,7 +199,13 @@ function run(db: DatabaseService, event: SessionEvent.Event) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell")))
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "shell"),
|
||||
isNotNull(SessionMessageTable.seq), // kilocode_change
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
@@ -124,7 +124,7 @@ export const SessionMessageTable = sqliteTable(
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionMessage.Type>().notNull(),
|
||||
seq: integer().notNull(),
|
||||
seq: integer(), // kilocode_change - allow released clients to share databases with newer schemas
|
||||
...Timestamps,
|
||||
data: text({ mode: "json" }).notNull().$type<SessionMessageData>(),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionStore from "./store"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq, isNotNull } from "drizzle-orm" // kilocode_change
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { SessionHistory } from "./history"
|
||||
@@ -45,7 +45,7 @@ export const layer = Layer.effect(
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(eq(SessionMessageTable.id, messageID))
|
||||
.where(and(eq(SessionMessageTable.id, messageID), isNotNull(SessionMessageTable.seq))) // kilocode_change
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
return row
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
|
||||
import { migrations } from "@opencode-ai/core/database/migration.gen"
|
||||
import legacyWriterMigration from "@opencode-ai/core/database/migration/20260714141136_session-message-legacy-writer-compat"
|
||||
import { Effect } from "effect"
|
||||
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
||||
import { sql } from "drizzle-orm"
|
||||
import path from "path"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
|
||||
const make = EffectDrizzleSqlite.makeWithDefaults()
|
||||
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||
Effect.runPromise(
|
||||
effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
|
||||
)
|
||||
|
||||
describe("database migration compatibility", () => {
|
||||
test("accepts released v7.4.7 session message writes after current migrations", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* make
|
||||
const split = migrations.findIndex((migration) => migration.id === "20260601010001_normalize_storage_paths")
|
||||
expect(split).toBeGreaterThan(0)
|
||||
yield* DatabaseMigration.applyOnly(db, migrations.slice(0, split))
|
||||
yield* db.run(
|
||||
sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('project', '/repo', 1, 1, '[]')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'project', 'session', '/repo', 'Session', '7.4.7', 1, 1)`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('legacy-message', 'session', 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('legacy-part', 'legacy-message', 'session', 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy-projection', 'session', 'user', 1, 1, '{}')`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, migrations.slice(split))
|
||||
|
||||
// This is the projection shape written by the CLI bundled with VS Code v7.4.7.
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('message', 'session', 'user', 1, 1, '{}')`,
|
||||
)
|
||||
yield* db.run(
|
||||
sql`UPDATE session_message SET data = '{"text":"updated"}' WHERE id = 'message'`,
|
||||
)
|
||||
|
||||
expect(yield* db.get(sql`SELECT id, seq, data FROM session_message WHERE id = 'message'`)).toEqual({
|
||||
id: "message",
|
||||
seq: null,
|
||||
data: '{"text":"updated"}',
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT id FROM session WHERE id = 'session'`)).toEqual({ id: "session" })
|
||||
expect(yield* db.get(sql`SELECT id FROM message WHERE id = 'legacy-message'`)).toEqual({ id: "legacy-message" })
|
||||
expect(yield* db.get(sql`SELECT id FROM part WHERE id = 'legacy-part'`)).toEqual({ id: "legacy-part" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves sequenced projections when repairing an already-migrated database", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* make
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL, FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE)`,
|
||||
)
|
||||
yield* db.run(sql`CREATE UNIQUE INDEX session_message_session_seq_idx ON session_message (session_id, seq)`)
|
||||
yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('sequenced', 'session', 'user', 7, 1, 1, '{}')`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [legacyWriterMigration])
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy', 'session', 'user', 2, 2, '{}')`,
|
||||
)
|
||||
|
||||
expect(yield* db.all(sql`SELECT id, seq FROM session_message ORDER BY id`)).toEqual([
|
||||
{ id: "legacy", seq: null },
|
||||
{ id: "sequenced", seq: 7 },
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("repairs a WAL database while preserving foreign keys and sequence uniqueness", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const filename = path.join(tmp.path, "kilo.db")
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* make
|
||||
yield* db.run(sql`PRAGMA journal_mode = WAL`)
|
||||
yield* db.run(sql`PRAGMA foreign_keys = ON`)
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL, FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE)`,
|
||||
)
|
||||
yield* db.run(sql`CREATE UNIQUE INDEX session_message_session_seq_idx ON session_message (session_id, seq)`)
|
||||
yield* db.run(sql`INSERT INTO session (id) VALUES ('session')`)
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('sequenced', 'session', 'user', 7, 1, 1, '{}')`,
|
||||
)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [legacyWriterMigration])
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('legacy-1', 'session', 'user', 2, 2, '{}'), ('legacy-2', 'session', 'user', 3, 3, '{}')`,
|
||||
)
|
||||
|
||||
expect(yield* db.all(sql`PRAGMA foreign_key_check`)).toEqual([])
|
||||
expect(
|
||||
yield* Effect.exit(
|
||||
db.run(
|
||||
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('duplicate', 'session', 'user', 7, 4, 4, '{}')`,
|
||||
),
|
||||
),
|
||||
).toMatchObject({ _tag: "Failure" })
|
||||
yield* db.run(sql`DELETE FROM session WHERE id = 'session'`)
|
||||
expect(yield* db.all(sql`SELECT id FROM session_message`)).toEqual([])
|
||||
}).pipe(Effect.provide(SqliteClient.layer({ filename })), Effect.scoped),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user