@morlay/session-rdb 0.0.15 → 0.0.16-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +11 -13
  2. package/dist/artifact.d.mts +30 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-GdKkSSb-.d.mts +239 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +763 -168
  10. package/dist/log-DBFUBPhv.mjs +394 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-CnWwGToZ.mjs +698 -0
  13. package/dist/storage.d.mts +2 -7
  14. package/dist/storage.mjs +2 -3
  15. package/dist/testing.d.mts +30 -1
  16. package/dist/testing.mjs +676 -1991
  17. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
  18. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
  19. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
  20. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
  21. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
  22. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
  23. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
  24. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
  25. package/package.json +17 -12
  26. package/src/adapters/index.ts +10 -2
  27. package/src/adapters/to-postgres.ts +19 -15
  28. package/src/adapters/to-sqlite.ts +20 -14
  29. package/src/{entities → adapters}/types.ts +7 -8
  30. package/src/backend.ts +0 -7
  31. package/src/branch.ts +29 -110
  32. package/src/drizzle/postgres-v2.ts +11 -0
  33. package/src/drizzle/postgres-v3.ts +11 -0
  34. package/src/drizzle/sqlite-v2.ts +10 -0
  35. package/src/drizzle/sqlite-v3.ts +11 -0
  36. package/src/entities/index.ts +2 -30
  37. package/src/entities/v2/index.ts +20 -0
  38. package/src/entities/v2/session-events.ts +11 -0
  39. package/src/entities/v3/events.ts +27 -0
  40. package/src/entities/v3/index.ts +27 -0
  41. package/src/entities/v3/persistence-state.ts +10 -0
  42. package/src/entities/v3/schema-meta.ts +9 -0
  43. package/src/entities/v3/session-events.ts +26 -0
  44. package/src/entities/v3/sessions.ts +20 -0
  45. package/src/import.ts +65 -57
  46. package/src/index.ts +929 -227
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +110 -0
  49. package/src/log.ts +143 -91
  50. package/src/postgres.ts +75 -93
  51. package/src/schema.ts +3 -14
  52. package/src/sqlite.ts +59 -46
  53. package/src/testing/contract.ts +460 -375
  54. package/src/testing/coordinator-contract.ts +108 -1374
  55. package/src/testing.ts +1 -6
  56. package/dist/index-uUvn6gYp.d.mts +0 -111
  57. package/dist/schema-vwzwEXNE.mjs +0 -878
  58. package/dist/sqlite-CG_Qcx5C.mjs +0 -356
  59. package/src/adapters/ddl.ts +0 -77
  60. package/src/entities/events.ts +0 -27
  61. package/src/entities/persistence-state.ts +0 -10
  62. package/src/entities/schema-meta.ts +0 -9
  63. package/src/entities/session-events.ts +0 -29
  64. package/src/entities/sessions.ts +0 -20
  65. package/src/migrate.ts +0 -137
package/src/postgres.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { and, desc, eq, gte, sql } from "drizzle-orm";
3
- import type { PgAsyncDatabase, PgAsyncTransaction, PgQueryResultHKT } from "drizzle-orm/pg-core";
2
+ import { readdirSync } from "node:fs";
3
+ import { and, eq, gte, sql } from "drizzle-orm";
4
+ import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
5
+ import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
6
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
4
7
  import type { SessionId } from "@deepseek-ai/dsh-session";
5
8
  import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
6
9
  import {
@@ -10,11 +13,13 @@ import {
10
13
  type EventRow,
11
14
  type SessionRow,
12
15
  } from "./backend.ts";
13
- import { SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID } from "./schema.ts";
14
- import { createTablesSql, toPostgresSchema } from "./adapters/index.ts";
16
+ import { toPostgresSchema } from "./adapters/index.ts";
15
17
  import { postgresTableDefs } from "./entities/index.ts";
16
18
  import { sessionConflictRow, sessionInsertRow } from "./log.ts";
17
19
 
20
+ /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
21
+ const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
22
+
18
23
  export interface PostgresBackendOptions {
19
24
  identityBase: string;
20
25
 
@@ -23,60 +28,36 @@ export interface PostgresBackendOptions {
23
28
  close: () => Promise<void>;
24
29
  }
25
30
 
26
- export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> implements Backend {
31
+ export class PostgresBackend implements Backend {
27
32
  readonly kind = "postgres" as const;
28
33
  storeIdentity!: string;
29
34
 
30
35
  private readonly tables: Record<string, any>;
31
36
 
32
37
  constructor(
33
- private readonly db: PgAsyncDatabase<THKT>,
38
+ private readonly db: NodePgDatabase,
34
39
  private readonly options: PostgresBackendOptions,
35
40
  ) {
36
41
  this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
37
42
  }
38
43
 
39
44
  async open(): Promise<void> {
40
- const storeId = await this.db.transaction(async (tx) => {
41
- const schema = this.options.schema ?? "public";
42
- // 探测在建表之前:t_schema_meta 存在与否区分全新库与已有库。
43
- // `to_regclass` 是数据库元数据查询,drizzle 无对应 API;schema 限定
44
- // 显式引用(不依赖 search_path)。
45
- const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
46
- const probe = (await tx.execute(
47
- sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
48
- )) as unknown as { rows: { exists: boolean }[] };
49
- const metaExists = probe.rows[0]?.exists === true;
50
- // DDL 一次一条(PG extended query protocol 拒绝多语句字符串),
51
- // 逐条执行保持初始化原子。
52
- for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) {
53
- await tx.execute(sql.raw(statement));
54
- }
55
- if (!metaExists) {
56
- await tx
57
- .insert(this.tables["t_schema_meta"])
58
- .values([
59
- { fKey: "schema_version", fValue: String(SCHEMA_VERSION) },
60
- { fKey: "application_id", fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) },
61
- ])
62
- .execute();
63
- }
64
- // 校验:缺版本行 = 有对象但未版本化 → 拒绝,不迁移。
65
- const version = await this.readMeta(tx, "schema_version");
66
- const applicationId = await this.readMeta(tx, "application_id");
67
- if (version === undefined || applicationId === undefined) {
68
- throw new Error("session database has an unversioned schema or application identity");
69
- }
70
- if (Number(version) !== SCHEMA_VERSION) {
71
- throw new Error(
72
- `session database has schema version ${version}, incompatible with this build (${SCHEMA_VERSION})`,
73
- );
74
- }
75
- if (Number(applicationId) !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
76
- throw new Error(
77
- `session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
78
- );
45
+ const schema = this.options.schema ?? "public";
46
+ // drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
47
+ // 本版本只执行 v3 diff(删 f_original_seq)。
48
+ const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
49
+ const probe = (await this.db.execute(
50
+ sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
51
+ )) as unknown as { rows: { exists: boolean }[] };
52
+ const metaExists = probe.rows[0]?.exists === true;
53
+ if (metaExists) {
54
+ const version = await this.readMeta(this.db, "schema_version");
55
+ if (version === "2") {
56
+ await this.baselineV2();
79
57
  }
58
+ }
59
+ await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
60
+ const storeId = await this.db.transaction(async (tx) => {
80
61
  await tx
81
62
  .insert(this.tables["t_persistence_state"])
82
63
  .values({ fSingleton: 1, fStoreId: randomUUID() })
@@ -87,15 +68,36 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
87
68
  .from(this.tables["t_persistence_state"])
88
69
  .where(eq(this.tables["t_persistence_state"].fSingleton, 1))
89
70
  .execute();
90
- const storeId = store[0]?.fStoreId;
91
- if (storeId === undefined || storeId.length === 0) {
71
+ const id = store[0]?.fStoreId;
72
+ if (id === undefined || id.length === 0) {
92
73
  throw new Error("session database has no valid store identity");
93
74
  }
94
- return storeId;
75
+ return id;
95
76
  });
96
77
  this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
97
78
  }
98
79
 
80
+ /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
81
+ private async baselineV2(): Promise<void> {
82
+ const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
83
+ if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/postgres");
84
+ await this.db.execute(sql`
85
+ CREATE SCHEMA IF NOT EXISTS drizzle
86
+ `);
87
+ await this.db.execute(sql`
88
+ CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
89
+ id SERIAL PRIMARY KEY,
90
+ hash text NOT NULL,
91
+ created_at bigint,
92
+ name text,
93
+ applied_at timestamp with time zone DEFAULT now()
94
+ )
95
+ `);
96
+ await this.db.execute(
97
+ sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`,
98
+ );
99
+ }
100
+
99
101
  async close(): Promise<void> {
100
102
  await this.options.close();
101
103
  }
@@ -110,17 +112,6 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
110
112
  )[0] as SessionRow | undefined;
111
113
  }
112
114
 
113
- async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
114
- return this.db
115
- .select({
116
- fSequence: this.tables["t_session_events"].fSequence,
117
- fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
118
- })
119
- .from(this.tables["t_session_events"])
120
- .where(eq(this.tables["t_session_events"].fSessionId, id))
121
- .execute();
122
- }
123
-
124
115
  async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
125
116
  const scoped =
126
117
  fromSequence === undefined
@@ -146,7 +137,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
146
137
  return this.db.transaction(async (tx) => fn(this.txFor(tx)));
147
138
  }
148
139
 
149
- private txFor(tx: PgAsyncTransaction<THKT>): BackendTx {
140
+ private txFor(tx: PgAsyncTransaction<NodePgQueryResultHKT>): BackendTx {
150
141
  return {
151
142
  upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
152
143
  getHead: (id) => this.getHead(tx, id),
@@ -159,13 +150,15 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
159
150
  bumpRevision: (id) => this.bumpRevision(tx, id),
160
151
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
161
152
  getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
162
- getLastBridge: (id) => this.getLastBridge(tx, id),
163
153
  };
164
154
  }
165
155
 
166
156
  // --- meta helpers ---
167
157
 
168
- private async readMeta(exec: PgAsyncDatabase<THKT>, key: string): Promise<string | undefined> {
158
+ private async readMeta(
159
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
160
+ key: string,
161
+ ): Promise<string | undefined> {
169
162
  const rows = await exec
170
163
  .select({ fValue: this.tables["t_schema_meta"].fValue })
171
164
  .from(this.tables["t_schema_meta"])
@@ -177,7 +170,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
177
170
  // --- row primitives (transaction-internal) ---
178
171
 
179
172
  private async upsertSession(
180
- exec: PgAsyncDatabase<THKT>,
173
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
181
174
  storage: SessionStorageMetadata,
182
175
  incarnation: string,
183
176
  ): Promise<void> {
@@ -192,7 +185,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
192
185
  }
193
186
 
194
187
  private async getHead(
195
- exec: PgAsyncDatabase<THKT>,
188
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
196
189
  id: SessionId,
197
190
  ): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
198
191
  const head = (
@@ -210,7 +203,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
210
203
  return head;
211
204
  }
212
205
 
213
- private async getSeedLength(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<number | null> {
206
+ private async getSeedLength(
207
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
208
+ id: SessionId,
209
+ ): Promise<number | null> {
214
210
  const row = (
215
211
  await exec
216
212
  .select({ fSeedLength: this.tables["t_sessions"].fSeedLength })
@@ -224,7 +220,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
224
220
  }
225
221
 
226
222
  private async updateSeedLength(
227
- exec: PgAsyncDatabase<THKT>,
223
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
228
224
  id: SessionId,
229
225
  seedLength: number,
230
226
  ): Promise<void> {
@@ -237,7 +233,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
237
233
 
238
234
  private static readonly INSERT_BATCH_ROWS = 1000;
239
235
 
240
- private async insertEvents(exec: PgAsyncDatabase<THKT>, events: EventInsert[]): Promise<void> {
236
+ private async insertEvents(
237
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
238
+ events: EventInsert[],
239
+ ): Promise<void> {
241
240
  if (events.length === 0) return;
242
241
  for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
243
242
  await exec
@@ -250,12 +249,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
250
249
  }
251
250
 
252
251
  private async insertBridges(
253
- exec: PgAsyncDatabase<THKT>,
252
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
254
253
  rows: Array<{
255
254
  fSessionId: SessionId;
256
255
  fEventId: string;
257
256
  fSequence: number;
258
- fOriginalSeq: number;
259
257
  fSurfaceOp: string | null;
260
258
  }>,
261
259
  ): Promise<void> {
@@ -269,7 +267,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
269
267
  }
270
268
 
271
269
  private async updateHead(
272
- exec: PgAsyncDatabase<THKT>,
270
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
273
271
  id: SessionId,
274
272
  headEventId: string,
275
273
  headSequence: number,
@@ -281,7 +279,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
281
279
  .execute();
282
280
  }
283
281
 
284
- private async bumpRevision(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<void> {
282
+ private async bumpRevision(
283
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
284
+ id: SessionId,
285
+ ): Promise<void> {
285
286
  await exec
286
287
  .update(this.tables["t_sessions"])
287
288
  .set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` })
@@ -290,7 +291,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
290
291
  }
291
292
 
292
293
  private async deleteBridgeTail(
293
- exec: PgAsyncDatabase<THKT>,
294
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
294
295
  id: SessionId,
295
296
  fromSequence: number,
296
297
  ): Promise<void> {
@@ -306,7 +307,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
306
307
  }
307
308
 
308
309
  private async getPrevBridge(
309
- exec: PgAsyncDatabase<THKT>,
310
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
310
311
  id: SessionId,
311
312
  sequence: number,
312
313
  ): Promise<{ fEventId: string; fSequence: number } | undefined> {
@@ -327,30 +328,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
327
328
  )[0] as { fEventId: string; fSequence: number } | undefined;
328
329
  }
329
330
 
330
- private async getLastBridge(
331
- exec: PgAsyncDatabase<THKT>,
332
- id: SessionId,
333
- ): Promise<{ fEventId: string; fSequence: number } | undefined> {
334
- return (
335
- await exec
336
- .select({
337
- fEventId: this.tables["t_session_events"].fEventId,
338
- fSequence: this.tables["t_session_events"].fSequence,
339
- })
340
- .from(this.tables["t_session_events"])
341
- .where(eq(this.tables["t_session_events"].fSessionId, id))
342
- .orderBy(desc(this.tables["t_session_events"].fSequence))
343
- .limit(1)
344
- .execute()
345
- )[0] as { fEventId: string; fSequence: number } | undefined;
346
- }
347
-
348
- private eventRows(exec: PgAsyncDatabase<THKT>) {
331
+ private eventRows(exec: PgAsyncDatabase<NodePgQueryResultHKT>) {
349
332
  return exec
350
333
  .select({
351
334
  fEventId: this.tables["t_session_events"].fEventId,
352
335
  fSequence: this.tables["t_session_events"].fSequence,
353
- fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
354
336
  fType: this.tables["t_events"].fType,
355
337
  fKind: this.tables["t_events"].fKind,
356
338
  fRole: this.tables["t_events"].fRole,
package/src/schema.ts CHANGED
@@ -2,12 +2,10 @@ import type { SessionEvent } from "@deepseek-ai/dsh-session";
2
2
  import { toSqliteSchema } from "./adapters/index.ts";
3
3
  import { sqliteTableDefs } from "./entities/index.ts";
4
4
 
5
- export const SCHEMA_VERSION = 2;
5
+ export const SCHEMA_VERSION = 3;
6
6
 
7
7
  export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
8
8
 
9
- export const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"] as const;
10
-
11
9
  export const EVENT_ENCODING = "json";
12
10
 
13
11
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -15,6 +13,8 @@ const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
15
13
 
16
14
  export const tPersistenceState = sqliteTables["t_persistence_state"]!;
17
15
 
16
+ export const tSchemaMeta = sqliteTables["t_schema_meta"]!;
17
+
18
18
  export const tSessions = sqliteTables["t_sessions"]!;
19
19
 
20
20
  export const tEvents = sqliteTables["t_events"]!;
@@ -29,17 +29,6 @@ export type JournalMode = "wal" | "delete" | "truncate" | "persist";
29
29
 
30
30
  export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
31
31
 
32
- export function isEphemeralType(type: string): boolean {
33
- return (EPHEMERAL_EVENT_TYPES as readonly string[]).includes(type);
34
- }
35
-
36
- export function isPersistedEvent(event: SessionEvent): boolean {
37
- return (
38
- !isEphemeralType(event.type) &&
39
- (event as SessionEvent & { ignorable?: unknown }).ignorable !== true
40
- );
41
- }
42
-
43
32
  export type EventKind =
44
33
  | "message"
45
34
  | "thinking"
package/src/sqlite.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { statSync } from "node:fs";
2
+ import { readdirSync, statSync } from "node:fs";
3
3
  import { mkdir, open } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
- import { and, desc, eq, gte, sql } from "drizzle-orm";
6
+ import { and, eq, gte, sql } from "drizzle-orm";
7
7
  import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
8
+ import { migrate } from "drizzle-orm/node-sqlite/migrator";
8
9
  import type { SessionId } from "@deepseek-ai/dsh-session";
9
10
  import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
10
11
  import {
@@ -14,10 +15,7 @@ import {
14
15
  type EventRow,
15
16
  type SessionRow,
16
17
  } from "./backend.ts";
17
- import { createTablesSql } from "./adapters/index.ts";
18
- import { sqliteTableDefs } from "./entities/index.ts";
19
18
  import { sessionConflictRow, sessionInsertRow } from "./log.ts";
20
- import { migrateSqliteV1ToV2 } from "./migrate.ts";
21
19
  import {
22
20
  DEFAULT_BUSY_TIMEOUT_MS,
23
21
  SCHEMA_VERSION,
@@ -31,6 +29,9 @@ import {
31
29
 
32
30
  type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
33
31
 
32
+ /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
33
+ const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url).pathname;
34
+
34
35
  const sqliteTxQueues = new Map<string, Promise<void>>();
35
36
 
36
37
  function enqueueSqliteTx<T>(path: string, fn: () => Promise<T>): Promise<T> {
@@ -64,6 +65,28 @@ export function openDatabase(
64
65
  const db = new DatabaseSync(path);
65
66
  try {
66
67
  configureDatabase(db, path, journalMode, busyTimeout);
68
+ const dbx = drizzle({ client: db });
69
+ const { user_version: onDisk } = db.prepare("PRAGMA user_version").get() as {
70
+ user_version: number;
71
+ };
72
+ // v2 库(旧版本建表)首次打开:建迁移表并标记 v2 baseline 已应用,
73
+ // 使 migrate 只执行 v3 diff(删 f_original_seq、建 t_schema_meta)。
74
+ if (onDisk === 2 && !hasMigrationsTable(db)) {
75
+ baselineV2(db);
76
+ }
77
+ migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
78
+ // store 身份单例:新库由 migrate 建表后插入;v2/v3 库已有行(no-op)。
79
+ dbx
80
+ .insert(tPersistenceState)
81
+ .values({ fSingleton: 1, fStoreId: randomUUID() })
82
+ .onConflictDoNothing()
83
+ .run();
84
+ if (onDisk === 0 || onDisk === 2) {
85
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
86
+ }
87
+ // journal_mode 不可绑定,经校验后的联合可直接插值;在归属校验、迁移
88
+ // 与身份写入全部成功之后应用(失败时保持磁盘原状)。
89
+ db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
67
90
  return db;
68
91
  } catch (error: unknown) {
69
92
  db.close();
@@ -71,6 +94,32 @@ export function openDatabase(
71
94
  }
72
95
  }
73
96
 
97
+ function hasMigrationsTable(db: DatabaseSync): boolean {
98
+ return (
99
+ db
100
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'")
101
+ .get() !== undefined
102
+ );
103
+ }
104
+
105
+ /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
106
+ function baselineV2(db: DatabaseSync): void {
107
+ const dir = readdirSync(sqliteMigrationsDir).find((name) => name.endsWith("_v2_initial"));
108
+ if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/sqlite");
109
+ db.exec(`
110
+ CREATE TABLE __drizzle_migrations (
111
+ id INTEGER PRIMARY KEY,
112
+ hash text NOT NULL,
113
+ created_at numeric,
114
+ name text,
115
+ applied_at TEXT
116
+ )
117
+ `);
118
+ db.prepare(
119
+ "INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES (?, ?, ?, ?)",
120
+ ).run("baseline", 0, dir, new Date().toISOString());
121
+ }
122
+
74
123
  function configureDatabase(
75
124
  db: DatabaseSync,
76
125
  path: string,
@@ -99,29 +148,16 @@ function configureDatabase(
99
148
  `session database at "${path}" has an unversioned schema or application identity`,
100
149
  );
101
150
  }
102
- if (onDisk === 1) {
103
- // 启动时自动迁移 v1 → v2(同一写锁事务内,失败整体回滚)。
104
- migrateSqliteV1ToV2(db);
105
- } else if (onDisk !== 0 && onDisk !== SCHEMA_VERSION) {
151
+ if (onDisk !== 0 && onDisk !== 2 && onDisk !== SCHEMA_VERSION) {
106
152
  throw new Error(
107
153
  `session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`,
108
154
  );
109
155
  }
110
- if (
111
- (onDisk === SCHEMA_VERSION || onDisk === 1) &&
112
- applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID
113
- ) {
156
+ if (onDisk >= 1 && applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
114
157
  throw new Error(
115
158
  `session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
116
159
  );
117
160
  }
118
- for (const statement of createTablesSql("sqlite", sqliteTableDefs)) {
119
- tx.run(sql.raw(statement));
120
- }
121
- tx.insert(tPersistenceState)
122
- .values({ fSingleton: 1, fStoreId: randomUUID() })
123
- .onConflictDoNothing()
124
- .run();
125
161
  if (onDisk === 0) {
126
162
  tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
127
163
  tx.run(sql.raw(`PRAGMA user_version = ${SCHEMA_VERSION}`));
@@ -129,9 +165,6 @@ function configureDatabase(
129
165
  },
130
166
  { behavior: "immediate" },
131
167
  );
132
- // journal_mode 不可绑定,经校验后的联合可直接插值;在归属校验与初始化
133
- // 提交之后应用。
134
- db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
135
168
  }
136
169
 
137
170
  export interface SqliteBackendOptions {
@@ -164,6 +197,9 @@ export class SqliteBackend implements Backend {
164
197
  this.db = drizzle({
165
198
  client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout),
166
199
  });
200
+ // drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
201
+ // 本版本只执行 v3 diff(删 f_original_seq、建 t_schema_meta)。
202
+ migrate(this.db, { migrationsFolder: sqliteMigrationsDir });
167
203
  });
168
204
  try {
169
205
  const row = this.db
@@ -203,14 +239,6 @@ export class SqliteBackend implements Backend {
203
239
  | undefined;
204
240
  }
205
241
 
206
- async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
207
- return this.db
208
- .select({ fSequence: tSessionEvents.fSequence, fOriginalSeq: tSessionEvents.fOriginalSeq })
209
- .from(tSessionEvents)
210
- .where(eq(tSessionEvents.fSessionId, id))
211
- .all();
212
- }
213
-
214
242
  async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
215
243
  const scoped =
216
244
  fromSequence === undefined
@@ -263,7 +291,6 @@ export class SqliteBackend implements Backend {
263
291
  bumpRevision: (id) => this.bumpRevision(id),
264
292
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
265
293
  getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
266
- getLastBridge: (id) => this.getLastBridge(id),
267
294
  };
268
295
 
269
296
  // --- row primitives (transaction-internal or standalone) ---
@@ -328,7 +355,6 @@ export class SqliteBackend implements Backend {
328
355
  fSessionId: SessionId;
329
356
  fEventId: string;
330
357
  fSequence: number;
331
- fOriginalSeq: number;
332
358
  fSurfaceOp: string | null;
333
359
  }>,
334
360
  ): Promise<void> {
@@ -379,24 +405,11 @@ export class SqliteBackend implements Backend {
379
405
  .get() as { fEventId: string; fSequence: number } | undefined;
380
406
  }
381
407
 
382
- private async getLastBridge(
383
- id: SessionId,
384
- ): Promise<{ fEventId: string; fSequence: number } | undefined> {
385
- return this.db
386
- .select({ fEventId: tSessionEvents.fEventId, fSequence: tSessionEvents.fSequence })
387
- .from(tSessionEvents)
388
- .where(eq(tSessionEvents.fSessionId, id))
389
- .orderBy(desc(tSessionEvents.fSequence))
390
- .limit(1)
391
- .get() as { fEventId: string; fSequence: number } | undefined;
392
- }
393
-
394
408
  private eventRows() {
395
409
  return this.db
396
410
  .select({
397
411
  fEventId: tSessionEvents.fEventId,
398
412
  fSequence: tSessionEvents.fSequence,
399
- fOriginalSeq: tSessionEvents.fOriginalSeq,
400
413
  fType: tEvents.fType,
401
414
  fKind: tEvents.fKind,
402
415
  fRole: tEvents.fRole,