@storyshelf/db-sqlite 0.10.0-next-1e4feb2 → 0.10.0-next-75a95bc

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.
@@ -39,7 +39,7 @@ function createBetterSqlite3Database(options) {
39
39
  const adapter = createDrizzleAdapter(db, {
40
40
  metadata: {
41
41
  name: "better-sqlite3",
42
- version: "0.10.0-next-1e4feb2",
42
+ version: "0.10.0-next-75a95bc",
43
43
  description: "better-sqlite3 database adapter (native + Drizzle)",
44
44
  kind: "better-sqlite3",
45
45
  category: "database"
@@ -39,7 +39,7 @@ function createBunSqliteDatabase(options) {
39
39
  const adapter = createDrizzleAdapter(db, {
40
40
  metadata: {
41
41
  name: "Bun SQLite",
42
- version: "0.10.0-next-1e4feb2",
42
+ version: "0.10.0-next-75a95bc",
43
43
  description: "Bun SQLite database adapter (bun:sqlite + Drizzle)",
44
44
  kind: "bun-sqlite",
45
45
  category: "database"
package/dist/d1.mjs CHANGED
@@ -24,7 +24,7 @@ function createD1Database(options) {
24
24
  return createDrizzleAdapter(db, {
25
25
  metadata: {
26
26
  name: "Cloudflare D1",
27
- version: "0.10.0-next-1e4feb2",
27
+ version: "0.10.0-next-75a95bc",
28
28
  description: "Cloudflare D1 database adapter (D1 binding + Drizzle)",
29
29
  kind: "d1",
30
30
  category: "database",
@@ -6,13 +6,22 @@ import { DatabaseAdapter, DrizzleAdapterOptions } from "@storyshelf/core/adapter
6
6
  * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`
7
7
  * rejects async callbacks. Statements run on the adapter's own connection,
8
8
  * so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one
9
- * transaction at a time (see `transaction-mutex.ts`).
9
+ * transaction at a time, and the adapter's plain statements take their turn in the same queue, so
10
+ * none lands inside a transaction it does not belong to (see `transaction-mutex.ts`).
10
11
  *
11
12
  * @param adapter - Adapter built by {@link createDrizzleAdapter}.
12
13
  * @param exec - Synchronous statement runner on the same connection.
13
14
  * @returns The adapter with `transact` replaced.
14
15
  */
15
16
  declare function withManualTransactions(adapter: DatabaseAdapter, exec: (sql: string) => void): DatabaseAdapter;
16
- declare function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter;
17
- export { createDrizzleAdapter, withManualTransactions };
17
+ /** What differs between the drivers sharing {@link createDrizzleAdapter}. */
18
+ interface DrizzleAdapterBehavior {
19
+ /**
20
+ * Plain statements take their turn in the transaction queue (`node:sqlite`, on one connection).
21
+ * Off for drivers with a connection per transaction (Turso, D1) and for lifecycle transaction handles.
22
+ */
23
+ isolateStatements?: boolean;
24
+ }
25
+ declare function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions, behavior?: DrizzleAdapterBehavior): DatabaseAdapter;
26
+ export { DrizzleAdapterBehavior, createDrizzleAdapter, withManualTransactions };
18
27
  //# sourceMappingURL=drizzle-factory.d.mts.map
@@ -2,6 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { createNestingGuard } from "./nesting-guard.mjs";
5
+ import { isolatePlainStatements } from "./statement-isolation.mjs";
5
6
  import { createTransactionMutex } from "./transaction-mutex.mjs";
6
7
  import { and, eq, getTableColumns } from "drizzle-orm";
7
8
  // src/drizzle-factory.ts
@@ -10,7 +11,8 @@ import { and, eq, getTableColumns } from "drizzle-orm";
10
11
  * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`
11
12
  * rejects async callbacks. Statements run on the adapter's own connection,
12
13
  * so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one
13
- * transaction at a time (see `transaction-mutex.ts`).
14
+ * transaction at a time, and the adapter's plain statements take their turn in the same queue, so
15
+ * none lands inside a transaction it does not belong to (see `transaction-mutex.ts`).
14
16
  *
15
17
  * @param adapter - Adapter built by {@link createDrizzleAdapter}.
16
18
  * @param exec - Synchronous statement runner on the same connection.
@@ -18,10 +20,10 @@ import { and, eq, getTableColumns } from "drizzle-orm";
18
20
  */
19
21
  function withManualTransactions(adapter, exec) {
20
22
  const mutex = createTransactionMutex(createNestingGuard());
21
- return {
23
+ return isolatePlainStatements({
22
24
  ...adapter,
23
25
  transact: async (fn) => await mutex.run(async () => await runManualTransaction(adapter, exec, fn))
24
- };
26
+ }, mutex);
25
27
  }
26
28
  async function runManualTransaction(adapter, exec, fn) {
27
29
  const tx = {
@@ -68,10 +70,10 @@ function buildLifecycle(options) {
68
70
  }
69
71
  };
70
72
  }
71
- function createDrizzleAdapter(db, options) {
73
+ function createDrizzleAdapter(db, options, behavior = {}) {
72
74
  const drizzle = db;
73
75
  const mutex = createTransactionMutex(createNestingGuard());
74
- return {
76
+ const adapter = {
75
77
  metadata: options.metadata,
76
78
  tables: options.tables,
77
79
  lifecycle: buildLifecycle(options),
@@ -87,6 +89,7 @@ function createDrizzleAdapter(db, options) {
87
89
  all: async (query) => await drizzle.all(query),
88
90
  transact: async (fn) => await mutex.run(async () => await drizzle.transaction(async (tx) => await fn(buildTxStore(tx))))
89
91
  };
92
+ return behavior.isolateStatements === true ? isolatePlainStatements(adapter, mutex) : adapter;
90
93
  }
91
94
  async function updateWhereOne(drizzle, table, id, values, where) {
92
95
  return await drizzle.update(table).set(values).where(and(eq(idOf(table), id), where) ?? where).returning().get() ?? await getOne(drizzle, table, id);
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle-factory.mjs","names":[],"sources":["../src/drizzle-factory.ts"],"sourcesContent":["/**\n * SQLite Drizzle adapter factory.\n *\n * Builds a {@link DatabaseAdapter} over any SQLite-compatible Drizzle dialect.\n */\n\nimport type {\n DatabaseAdapter,\n DrizzleAdapterOptions,\n ListOptions,\n TxStore,\n} from \"@storyshelf/core/adapter/database\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { and, eq, getTableColumns } from \"drizzle-orm\";\nimport type { AnySQLiteTable, SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport { createNestingGuard } from \"./nesting-guard.ts\";\nimport { createTransactionMutex } from \"./transaction-mutex.ts\";\n\n/**\n * Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous\n * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`\n * rejects async callbacks. Statements run on the adapter's own connection,\n * so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one\n * transaction at a time (see `transaction-mutex.ts`).\n *\n * @param adapter - Adapter built by {@link createDrizzleAdapter}.\n * @param exec - Synchronous statement runner on the same connection.\n * @returns The adapter with `transact` replaced.\n */\nexport function withManualTransactions(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n): DatabaseAdapter {\n const mutex = createTransactionMutex(createNestingGuard());\n return {\n ...adapter,\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(async () => await runManualTransaction(adapter, exec, fn)),\n };\n}\n\nasync function runManualTransaction<R>(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n fn: (tx: TxStore) => Promise<R>,\n): Promise<R> {\n const tx: TxStore = {\n insert: async (table, values) => await adapter.insert(table, values),\n update: async (table, id, values) => await adapter.update(table, id, values),\n get: async (table, id) => await adapter.get(table, id),\n remove: async (table, id) => {\n await adapter.remove(table, id);\n },\n list: async (table, opts) => await adapter.list(table, opts),\n count: async (table, where) => await adapter.count(table, where),\n };\n exec(\"BEGIN\");\n try {\n const result = await fn(tx);\n exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n exec(\"ROLLBACK\");\n } catch {\n // already rolled back (e.g. constraint abort) — report the cause\n }\n throw error;\n }\n}\n\n/** Setup, teardown (once) and health for the adapter; teardown closes the driver a single time. */\nfunction buildLifecycle(options: DrizzleAdapterOptions): NonNullable<DatabaseAdapter[\"lifecycle\"]> {\n let closed = false;\n return {\n setup: async (context) => {\n await options.migrate({ logger: context.logger });\n },\n teardown: async () => {\n if (closed) {\n return;\n }\n closed = true;\n await options.close();\n },\n health: async () => {\n if (closed) {\n return { ok: false, detail: \"database closed\" };\n }\n if (options.ping) {\n await options.ping();\n }\n return { ok: true };\n },\n };\n}\n\nexport function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter {\n const drizzle = db as DrizzleLike;\n // The shared connection (or, for a libSQL file, its connection per transaction) cannot overlap\n // transactions: they queue (see `transaction-mutex.ts`).\n const mutex = createTransactionMutex(createNestingGuard());\n return {\n metadata: options.metadata,\n tables: options.tables,\n lifecycle: buildLifecycle(options),\n insert: async <T extends AnySQLiteTable>(\n table: T,\n values: T[\"$inferInsert\"],\n ): Promise<T[\"$inferSelect\"]> => await insertOne(drizzle, table, values),\n update: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n // Homomorphic equivalent of Partial<T[\"$inferInsert\"]>; written out so\n // the type stays deferred instead of resolving to `{}` under generics.\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n ): Promise<T[\"$inferSelect\"]> => await updateOne(drizzle, table, id, values),\n updateWhere: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n ): Promise<T[\"$inferSelect\"] | null> => await updateWhereOne(drizzle, table, id, values, where),\n get: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n ): Promise<T[\"$inferSelect\"] | null> => await getOne(drizzle, table, id),\n remove: async (table: AnySQLiteTable, id: string): Promise<void> => {\n await removeOne(drizzle, table, id);\n },\n list: async <T extends AnySQLiteTable>(\n table: T,\n opts: ListOptions = {},\n ): Promise<T[\"$inferSelect\"][]> => await listMany(drizzle, table, opts),\n count: async (table: AnySQLiteTable, where?: SQL): Promise<number> =>\n await drizzle.$count(table, where),\n all: async <T>(query: SQL): Promise<T[]> => await drizzle.all(query),\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(\n async () =>\n await drizzle.transaction(async (tx) => await fn(buildTxStore(tx as DrizzleLike))),\n ),\n };\n}\n\nasync function updateWhereOne<T extends AnySQLiteTable>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n): Promise<T[\"$inferSelect\"] | null> {\n const updated = (await drizzle\n .update(table)\n .set(values)\n .where(and(eq(idOf(table), id), where) ?? where)\n .returning()\n .get()) as T[\"$inferSelect\"] | undefined;\n return updated ?? (await getOne(drizzle, table, id));\n}\n\nasync function insertOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n values: T[\"$inferInsert\"],\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle.insert(table).values(values).returning().get()) as T[\"$inferSelect\"];\n}\n\nasync function updateOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle\n .update(table)\n .set(values)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .returning()\n .get()) as T[\"$inferSelect\"];\n}\n\nasync function getOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n): Promise<T[\"$inferSelect\"] | null> {\n return (\n ((await drizzle\n .select()\n .from(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .limit(1)\n .get()) as T[\"$inferSelect\"] | undefined) ?? null\n );\n}\n\nasync function removeOne(drizzle: DrizzleLike, table: Table, id: string): Promise<void> {\n await drizzle\n .delete(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .run();\n}\n\nasync function listMany<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n opts: ListOptions = {},\n): Promise<T[\"$inferSelect\"][]> {\n const query = applyListOptions(drizzle.select().from(table), opts);\n const rows: unknown = await query.all();\n return rows as T[\"$inferSelect\"][];\n}\n\n/** The `where`, `orderBy`, `limit` and `offset` the caller asked for, in that order. */\nfunction applyListOptions(query: DrizzleSelectChain, opts: ListOptions): DrizzleSelectChain {\n let next = query;\n if (opts.where) {\n next = next.where(opts.where);\n }\n if (opts.orderBy) {\n next = next.orderBy(opts.orderBy);\n }\n if (opts.limit !== undefined) {\n next = next.limit(opts.limit);\n }\n if (opts.offset !== undefined) {\n next = next.offset(opts.offset);\n }\n return next;\n}\n\n/** CRUD subset bound to a transaction handle. */\nfunction buildTxStore(drizzle: DrizzleLike): TxStore {\n return {\n insert: async (table, values) => await insertOne(drizzle, table, values as never),\n update: async (table, id, values) => await updateOne(drizzle, table, id, values as never),\n get: async (table, id) => await getOne(drizzle, table, id),\n remove: async (table, id) => {\n await removeOne(drizzle, table, id);\n },\n list: async (table, opts) => await listMany(drizzle, table, opts),\n count: async (table, where) => await drizzle.$count(table as AnySQLiteTable, where),\n };\n}\n\n/** A value or a promise of one (sync node:sqlite vs async libSQL drivers). */\ntype MaybePromise<T> = T | Promise<T>;\n\n/** Chainable select surface shared by the sync and async Drizzle dialects. */\ninterface DrizzleSelectChain {\n where(where: SQL): DrizzleSelectChain;\n orderBy(order: SQL): DrizzleSelectChain;\n limit(n: number): DrizzleSelectChain;\n offset(n: number): DrizzleSelectChain;\n get(): MaybePromise<unknown>;\n all(): MaybePromise<unknown[]>;\n}\n\n/**\n * Minimal Drizzle surface consumed by {@link createDrizzleAdapter}. Drivers\n * pass their dialect instance as `unknown`; the cast lives here, once, so\n * driver packages stay fully typed. Covered by the adapter contract tests.\n */\ninterface DrizzleLike {\n insert(table: AnySQLiteTable): {\n values(values: unknown): { returning(): { get(): MaybePromise<unknown> } };\n };\n update(table: AnySQLiteTable): {\n set(values: unknown): { where(where: SQL): { returning(): { get(): MaybePromise<unknown> } } };\n };\n select(): { from(table: AnySQLiteTable): DrizzleSelectChain };\n delete(table: AnySQLiteTable): { where(where: SQL): { run(): MaybePromise<unknown> } };\n $count(table: AnySQLiteTable, where?: SQL): MaybePromise<number>;\n all<T>(query: SQL): MaybePromise<T[]>;\n transaction<R>(fn: (tx: unknown) => Promise<R>): Promise<R>;\n}\n\nfunction idOf(table: AnySQLiteTable): SQLiteColumn {\n const id = getTableColumns(table)[\"id\"];\n if (!id) {\n throw new Error(\"Table has no id column\");\n }\n return id;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"drizzle-factory.mjs","names":[],"sources":["../src/drizzle-factory.ts"],"sourcesContent":["/**\n * SQLite Drizzle adapter factory.\n *\n * Builds a {@link DatabaseAdapter} over any SQLite-compatible Drizzle dialect.\n */\n\nimport type {\n DatabaseAdapter,\n DrizzleAdapterOptions,\n ListOptions,\n TxStore,\n} from \"@storyshelf/core/adapter/database\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { and, eq, getTableColumns } from \"drizzle-orm\";\nimport type { AnySQLiteTable, SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport { createNestingGuard } from \"./nesting-guard.ts\";\nimport { isolatePlainStatements } from \"./statement-isolation.ts\";\nimport { createTransactionMutex } from \"./transaction-mutex.ts\";\n\n/**\n * Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous\n * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`\n * rejects async callbacks. Statements run on the adapter's own connection,\n * so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one\n * transaction at a time, and the adapter's plain statements take their turn in the same queue, so\n * none lands inside a transaction it does not belong to (see `transaction-mutex.ts`).\n *\n * @param adapter - Adapter built by {@link createDrizzleAdapter}.\n * @param exec - Synchronous statement runner on the same connection.\n * @returns The adapter with `transact` replaced.\n */\nexport function withManualTransactions(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n): DatabaseAdapter {\n const mutex = createTransactionMutex(createNestingGuard());\n // `tx` calls the inner adapter below directly; only the outer one's plain methods take turns.\n return isolatePlainStatements(\n {\n ...adapter,\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(async () => await runManualTransaction(adapter, exec, fn)),\n },\n mutex,\n );\n}\n\nasync function runManualTransaction<R>(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n fn: (tx: TxStore) => Promise<R>,\n): Promise<R> {\n const tx: TxStore = {\n insert: async (table, values) => await adapter.insert(table, values),\n update: async (table, id, values) => await adapter.update(table, id, values),\n get: async (table, id) => await adapter.get(table, id),\n remove: async (table, id) => {\n await adapter.remove(table, id);\n },\n list: async (table, opts) => await adapter.list(table, opts),\n count: async (table, where) => await adapter.count(table, where),\n };\n exec(\"BEGIN\");\n try {\n const result = await fn(tx);\n exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n exec(\"ROLLBACK\");\n } catch {\n // already rolled back (e.g. constraint abort) — report the cause\n }\n throw error;\n }\n}\n\n/** Setup, teardown (once) and health for the adapter; teardown closes the driver a single time. */\nfunction buildLifecycle(options: DrizzleAdapterOptions): NonNullable<DatabaseAdapter[\"lifecycle\"]> {\n let closed = false;\n return {\n setup: async (context) => {\n await options.migrate({ logger: context.logger });\n },\n teardown: async () => {\n if (closed) {\n return;\n }\n closed = true;\n await options.close();\n },\n health: async () => {\n if (closed) {\n return { ok: false, detail: \"database closed\" };\n }\n if (options.ping) {\n await options.ping();\n }\n return { ok: true };\n },\n };\n}\n\n/** What differs between the drivers sharing {@link createDrizzleAdapter}. */\nexport interface DrizzleAdapterBehavior {\n /**\n * Plain statements take their turn in the transaction queue (`node:sqlite`, on one connection).\n * Off for drivers with a connection per transaction (Turso, D1) and for lifecycle transaction handles.\n */\n isolateStatements?: boolean;\n}\n\nexport function createDrizzleAdapter(\n db: unknown,\n options: DrizzleAdapterOptions,\n behavior: DrizzleAdapterBehavior = {},\n): DatabaseAdapter {\n const drizzle = db as DrizzleLike;\n // The shared connection (or, for a libSQL file, its connection per transaction) cannot overlap\n // transactions: they queue (see `transaction-mutex.ts`).\n const mutex = createTransactionMutex(createNestingGuard());\n const adapter: DatabaseAdapter = {\n metadata: options.metadata,\n tables: options.tables,\n lifecycle: buildLifecycle(options),\n insert: async <T extends AnySQLiteTable>(\n table: T,\n values: T[\"$inferInsert\"],\n ): Promise<T[\"$inferSelect\"]> => await insertOne(drizzle, table, values),\n update: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n // Homomorphic equivalent of Partial<T[\"$inferInsert\"]>; written out so\n // the type stays deferred instead of resolving to `{}` under generics.\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n ): Promise<T[\"$inferSelect\"]> => await updateOne(drizzle, table, id, values),\n updateWhere: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n ): Promise<T[\"$inferSelect\"] | null> => await updateWhereOne(drizzle, table, id, values, where),\n get: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n ): Promise<T[\"$inferSelect\"] | null> => await getOne(drizzle, table, id),\n remove: async (table: AnySQLiteTable, id: string): Promise<void> => {\n await removeOne(drizzle, table, id);\n },\n list: async <T extends AnySQLiteTable>(\n table: T,\n opts: ListOptions = {},\n ): Promise<T[\"$inferSelect\"][]> => await listMany(drizzle, table, opts),\n count: async (table: AnySQLiteTable, where?: SQL): Promise<number> =>\n await drizzle.$count(table, where),\n all: async <T>(query: SQL): Promise<T[]> => await drizzle.all(query),\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(\n async () =>\n await drizzle.transaction(async (tx) => await fn(buildTxStore(tx as DrizzleLike))),\n ),\n };\n return behavior.isolateStatements === true ? isolatePlainStatements(adapter, mutex) : adapter;\n}\n\nasync function updateWhereOne<T extends AnySQLiteTable>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n): Promise<T[\"$inferSelect\"] | null> {\n const updated = (await drizzle\n .update(table)\n .set(values)\n .where(and(eq(idOf(table), id), where) ?? where)\n .returning()\n .get()) as T[\"$inferSelect\"] | undefined;\n return updated ?? (await getOne(drizzle, table, id));\n}\n\nasync function insertOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n values: T[\"$inferInsert\"],\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle.insert(table).values(values).returning().get()) as T[\"$inferSelect\"];\n}\n\nasync function updateOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle\n .update(table)\n .set(values)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .returning()\n .get()) as T[\"$inferSelect\"];\n}\n\nasync function getOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n): Promise<T[\"$inferSelect\"] | null> {\n return (\n ((await drizzle\n .select()\n .from(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .limit(1)\n .get()) as T[\"$inferSelect\"] | undefined) ?? null\n );\n}\n\nasync function removeOne(drizzle: DrizzleLike, table: Table, id: string): Promise<void> {\n await drizzle\n .delete(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .run();\n}\n\nasync function listMany<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n opts: ListOptions = {},\n): Promise<T[\"$inferSelect\"][]> {\n const query = applyListOptions(drizzle.select().from(table), opts);\n const rows: unknown = await query.all();\n return rows as T[\"$inferSelect\"][];\n}\n\n/** The `where`, `orderBy`, `limit` and `offset` the caller asked for, in that order. */\nfunction applyListOptions(query: DrizzleSelectChain, opts: ListOptions): DrizzleSelectChain {\n let next = query;\n if (opts.where) {\n next = next.where(opts.where);\n }\n if (opts.orderBy) {\n next = next.orderBy(opts.orderBy);\n }\n if (opts.limit !== undefined) {\n next = next.limit(opts.limit);\n }\n if (opts.offset !== undefined) {\n next = next.offset(opts.offset);\n }\n return next;\n}\n\n/** CRUD subset bound to a transaction handle. */\nfunction buildTxStore(drizzle: DrizzleLike): TxStore {\n return {\n insert: async (table, values) => await insertOne(drizzle, table, values as never),\n update: async (table, id, values) => await updateOne(drizzle, table, id, values as never),\n get: async (table, id) => await getOne(drizzle, table, id),\n remove: async (table, id) => {\n await removeOne(drizzle, table, id);\n },\n list: async (table, opts) => await listMany(drizzle, table, opts),\n count: async (table, where) => await drizzle.$count(table as AnySQLiteTable, where),\n };\n}\n\n/** A value or a promise of one (sync node:sqlite vs async libSQL drivers). */\ntype MaybePromise<T> = T | Promise<T>;\n\n/** Chainable select surface shared by the sync and async Drizzle dialects. */\ninterface DrizzleSelectChain {\n where(where: SQL): DrizzleSelectChain;\n orderBy(order: SQL): DrizzleSelectChain;\n limit(n: number): DrizzleSelectChain;\n offset(n: number): DrizzleSelectChain;\n get(): MaybePromise<unknown>;\n all(): MaybePromise<unknown[]>;\n}\n\n/**\n * Minimal Drizzle surface consumed by {@link createDrizzleAdapter}. Drivers\n * pass their dialect instance as `unknown`; the cast lives here, once, so\n * driver packages stay fully typed. Covered by the adapter contract tests.\n */\ninterface DrizzleLike {\n insert(table: AnySQLiteTable): {\n values(values: unknown): { returning(): { get(): MaybePromise<unknown> } };\n };\n update(table: AnySQLiteTable): {\n set(values: unknown): { where(where: SQL): { returning(): { get(): MaybePromise<unknown> } } };\n };\n select(): { from(table: AnySQLiteTable): DrizzleSelectChain };\n delete(table: AnySQLiteTable): { where(where: SQL): { run(): MaybePromise<unknown> } };\n $count(table: AnySQLiteTable, where?: SQL): MaybePromise<number>;\n all<T>(query: SQL): MaybePromise<T[]>;\n transaction<R>(fn: (tx: unknown) => Promise<R>): Promise<R>;\n}\n\nfunction idOf(table: AnySQLiteTable): SQLiteColumn {\n const id = getTableColumns(table)[\"id\"];\n if (!id) {\n throw new Error(\"Table has no id column\");\n }\n return id;\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -32,7 +32,7 @@ function createSqliteDatabase(path) {
32
32
  const adapter = createDrizzleAdapter(db, {
33
33
  metadata: {
34
34
  name: "SQLite",
35
- version: "0.10.0-next-1e4feb2",
35
+ version: "0.10.0-next-75a95bc",
36
36
  description: "SQLite database adapter (node:sqlite + Drizzle)",
37
37
  kind: "sqlite",
38
38
  category: "database"
@@ -49,7 +49,7 @@ function createSqliteDatabase(path) {
49
49
  ping: () => {
50
50
  sqlite.prepare("SELECT 1").get();
51
51
  }
52
- });
52
+ }, { isolateStatements: true });
53
53
  return attachRetention(adapter, retention);
54
54
  }
55
55
  const WEBHOOK_SECRET_DROP = "ALTER TABLE webhooks DROP COLUMN secret";
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { drizzle, type AsyncRemoteCallback } from \"drizzle-orm/sqlite-proxy\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { commentsRebuildStatements } from \"./comments-rebuild.ts\";\nimport { DDL, IDENTITY_STATEMENTS, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { ensureDbDir } from \"./ensure-db-dir.ts\";\nimport { rollbackQuietly } from \"./lifecycle-transaction.ts\";\nimport {\n execUnless,\n isDuplicateColumn,\n isMissingColumn,\n reportSkippedIndex,\n toleratedFor,\n} from \"./migration-errors.ts\";\nimport {\n activateRetention,\n attachRetention,\n type RetentionActivation,\n} from \"./retention-activation.ts\";\nimport { assertReviewSchema } from \"./review-schema-check.ts\";\nimport { schema } from \"./schema/index.ts\";\nimport { parseTableInfo, type TableColumns } from \"./table-info.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Create a SQLite-backed DatabaseAdapter using node:sqlite and Drizzle ORM.\n *\n * @param path - Filesystem path to the SQLite database file.\n * @returns A DatabaseAdapter backed by the given SQLite file (WAL mode).\n */\nexport function createSqliteDatabase(path: string): DatabaseAdapter {\n ensureDbDir(path);\n const sqlite = new DatabaseSync(path, { returnArrays: true });\n sqlite.exec(\"PRAGMA journal_mode = WAL\");\n sqlite.exec(\"PRAGMA busy_timeout = 5000\");\n const callback = createCallback(sqlite);\n const db = drizzle(callback, { schema });\n const retention: RetentionActivation = {};\n\n const adapter = createDrizzleAdapter(db, {\n metadata: {\n name: \"SQLite\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"SQLite database adapter (node:sqlite + Drizzle)\",\n kind: \"sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: async ({ logger }) => {\n await assertReviewSchema(async (table) => await Promise.resolve(tableInfo(sqlite, table)));\n runMigrations(sqlite, logger);\n await activateRetention(path, retention);\n },\n close: () => {\n sqlite.close();\n },\n ping: () => {\n sqlite.prepare(\"SELECT 1\").get();\n },\n });\n return attachRetention(adapter, retention);\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/**\n * Re-runs share `(project_id, git_sha)` by design (ULID build ids), so the\n * lookup index must not be unique. Existing databases created it UNIQUE —\n * drop and recreate it plain.\n */\nconst BUILDS_GITSHA_DEDUP = \"DROP INDEX IF EXISTS builds_project_gitsha_idx\";\nconst BUILDS_GITSHA_INDEX =\n \"CREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha)\";\n\ntype ProxyMethod = \"run\" | \"all\" | \"values\" | \"get\";\n\n/**\n * Execute one Drizzle statement against DatabaseSync, returning proxy-shaped rows.\n * Drizzle builds the SQL and params; only the transport is hand-written.\n * `values` shares the `all` shape (array rows); the factory never emits it.\n * Note: for `get`, the proxy passes `result.rows` straight to the row mapper,\n * so `rows` is the single row (or undefined on a miss) — not a one-row array.\n */\nfunction executeStatement(\n sqlite: DatabaseSync,\n sql: string,\n params: unknown[],\n method: ProxyMethod,\n): { rows: unknown } {\n const stmt = sqlite.prepare(sql);\n // Single contained cast: Drizzle's params are SQLite scalars by construction.\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the narrowing; the rule misfires on the variadic overloads\n const args = params as SQLInputValue[];\n if (method === \"run\") {\n stmt.run(...args);\n return { rows: [] };\n }\n if (method === \"get\") {\n return { rows: stmt.get(...args) };\n }\n return { rows: stmt.all(...args) };\n}\n\nexport function createCallback(sqlite: DatabaseSync): AsyncRemoteCallback {\n const callback = (async (sql: string, params: unknown[], method: ProxyMethod) => {\n await Promise.resolve();\n return executeStatement(sqlite, sql, params, method);\n }) as AsyncRemoteCallback;\n return callback;\n}\n\nfunction migrateCommentsTable(sqlite: DatabaseSync): void {\n // The connection opens with `returnArrays`: a row is `[sql]`, not `{ sql }`. (Reading it as an\n // object threw here on every boot, and the catch-all around the call hid that the rebuild never ran.)\n const row = sqlite\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as unknown as [string] | undefined;\n if (!row?.[0].includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n swapCommentsTable(sqlite);\n}\n\n/** Swap `comments` for a rebuilt table in one transaction, with foreign keys off for the swap. */\nfunction swapCommentsTable(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n sqlite.exec(\"BEGIN\");\n rebuildComments(sqlite);\n sqlite.exec(\"COMMIT\");\n } catch (error) {\n rollbackQuietly(sqlite);\n throw error;\n } finally {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Swap `comments` for the current table, whose `user_id` may be null (inside one transaction). */\nfunction rebuildComments(sqlite: DatabaseSync): void {\n for (const statement of commentsRebuildStatements()) {\n sqlite.exec(statement);\n }\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(sqlite: DatabaseSync, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = sqlite.prepare(`PRAGMA table_info(${table})`).all() as unknown as unknown[][];\n const existing = new Set(rows.map((row) => String(row[1])));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execUnless(sqlite, `ALTER TABLE ${table} ADD COLUMN ${column}`, isDuplicateColumn);\n }\n }\n }\n}\n\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(sqlite: DatabaseSync, logger: Logger | undefined): void {\n for (const statement of IDENTITY_STATEMENTS) {\n if (execUnless(sqlite, statement, toleratedFor(statement))) {\n // The connection opens with `returnArrays`: the count is the first value of the row.\n reportSkippedIndex(\n statement,\n (sql) => Number((sqlite.prepare(sql).get() as unknown as [number | bigint])[0]),\n logger,\n );\n }\n }\n sqlite.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\n}\n\nfunction runMigrations(sqlite: DatabaseSync, logger: Logger | undefined): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n applyIdentityStatements(sqlite, logger);\n dropWebhookSecret(sqlite);\n migrateBuildsGitShaIndex(sqlite);\n migrateCommentsTable(sqlite);\n}\n\n/** The columns of a table, from `PRAGMA table_info` (rows are arrays: the adapter opens with `returnArrays`). */\nfunction tableInfo(sqlite: DatabaseSync, table: string): TableColumns {\n return parseTableInfo(sqlite.prepare(`PRAGMA table_info(${table})`).all());\n}\n\n/** Drop the retired `webhooks.secret` column; a database that no longer has it is already migrated. */\nfunction dropWebhookSecret(sqlite: DatabaseSync): void {\n execUnless(sqlite, WEBHOOK_SECRET_DROP, isMissingColumn);\n}\n\n/** Downgrade a legacy UNIQUE project/sha index to a plain lookup index. */\nfunction migrateBuildsGitShaIndex(sqlite: DatabaseSync): void {\n sqlite.exec(BUILDS_GITSHA_DEDUP);\n sqlite.exec(BUILDS_GITSHA_INDEX);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { drizzle, type AsyncRemoteCallback } from \"drizzle-orm/sqlite-proxy\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { commentsRebuildStatements } from \"./comments-rebuild.ts\";\nimport { DDL, IDENTITY_STATEMENTS, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { ensureDbDir } from \"./ensure-db-dir.ts\";\nimport { rollbackQuietly } from \"./lifecycle-transaction.ts\";\nimport {\n execUnless,\n isDuplicateColumn,\n isMissingColumn,\n reportSkippedIndex,\n toleratedFor,\n} from \"./migration-errors.ts\";\nimport {\n activateRetention,\n attachRetention,\n type RetentionActivation,\n} from \"./retention-activation.ts\";\nimport { assertReviewSchema } from \"./review-schema-check.ts\";\nimport { schema } from \"./schema/index.ts\";\nimport { parseTableInfo, type TableColumns } from \"./table-info.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Create a SQLite-backed DatabaseAdapter using node:sqlite and Drizzle ORM.\n *\n * @param path - Filesystem path to the SQLite database file.\n * @returns A DatabaseAdapter backed by the given SQLite file (WAL mode).\n */\nexport function createSqliteDatabase(path: string): DatabaseAdapter {\n ensureDbDir(path);\n const sqlite = new DatabaseSync(path, { returnArrays: true });\n sqlite.exec(\"PRAGMA journal_mode = WAL\");\n sqlite.exec(\"PRAGMA busy_timeout = 5000\");\n const callback = createCallback(sqlite);\n const db = drizzle(callback, { schema });\n const retention: RetentionActivation = {};\n\n const adapter = createDrizzleAdapter(\n db,\n {\n metadata: {\n name: \"SQLite\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"SQLite database adapter (node:sqlite + Drizzle)\",\n kind: \"sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: async ({ logger }) => {\n await assertReviewSchema(async (table) => await Promise.resolve(tableInfo(sqlite, table)));\n runMigrations(sqlite, logger);\n await activateRetention(path, retention);\n },\n close: () => {\n sqlite.close();\n },\n ping: () => {\n sqlite.prepare(\"SELECT 1\").get();\n },\n },\n // One connection: plain statements take their turn behind an open transaction.\n { isolateStatements: true },\n );\n return attachRetention(adapter, retention);\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/**\n * Re-runs share `(project_id, git_sha)` by design (ULID build ids), so the\n * lookup index must not be unique. Existing databases created it UNIQUE —\n * drop and recreate it plain.\n */\nconst BUILDS_GITSHA_DEDUP = \"DROP INDEX IF EXISTS builds_project_gitsha_idx\";\nconst BUILDS_GITSHA_INDEX =\n \"CREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha)\";\n\ntype ProxyMethod = \"run\" | \"all\" | \"values\" | \"get\";\n\n/**\n * Execute one Drizzle statement against DatabaseSync, returning proxy-shaped rows.\n * Drizzle builds the SQL and params; only the transport is hand-written.\n * `values` shares the `all` shape (array rows); the factory never emits it.\n * Note: for `get`, the proxy passes `result.rows` straight to the row mapper,\n * so `rows` is the single row (or undefined on a miss) — not a one-row array.\n */\nfunction executeStatement(\n sqlite: DatabaseSync,\n sql: string,\n params: unknown[],\n method: ProxyMethod,\n): { rows: unknown } {\n const stmt = sqlite.prepare(sql);\n // Single contained cast: Drizzle's params are SQLite scalars by construction.\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the narrowing; the rule misfires on the variadic overloads\n const args = params as SQLInputValue[];\n if (method === \"run\") {\n stmt.run(...args);\n return { rows: [] };\n }\n if (method === \"get\") {\n return { rows: stmt.get(...args) };\n }\n return { rows: stmt.all(...args) };\n}\n\nexport function createCallback(sqlite: DatabaseSync): AsyncRemoteCallback {\n const callback = (async (sql: string, params: unknown[], method: ProxyMethod) => {\n await Promise.resolve();\n return executeStatement(sqlite, sql, params, method);\n }) as AsyncRemoteCallback;\n return callback;\n}\n\nfunction migrateCommentsTable(sqlite: DatabaseSync): void {\n // The connection opens with `returnArrays`: a row is `[sql]`, not `{ sql }`. (Reading it as an\n // object threw here on every boot, and the catch-all around the call hid that the rebuild never ran.)\n const row = sqlite\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as unknown as [string] | undefined;\n if (!row?.[0].includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n swapCommentsTable(sqlite);\n}\n\n/** Swap `comments` for a rebuilt table in one transaction, with foreign keys off for the swap. */\nfunction swapCommentsTable(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n sqlite.exec(\"BEGIN\");\n rebuildComments(sqlite);\n sqlite.exec(\"COMMIT\");\n } catch (error) {\n rollbackQuietly(sqlite);\n throw error;\n } finally {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Swap `comments` for the current table, whose `user_id` may be null (inside one transaction). */\nfunction rebuildComments(sqlite: DatabaseSync): void {\n for (const statement of commentsRebuildStatements()) {\n sqlite.exec(statement);\n }\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(sqlite: DatabaseSync, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = sqlite.prepare(`PRAGMA table_info(${table})`).all() as unknown as unknown[][];\n const existing = new Set(rows.map((row) => String(row[1])));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execUnless(sqlite, `ALTER TABLE ${table} ADD COLUMN ${column}`, isDuplicateColumn);\n }\n }\n }\n}\n\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(sqlite: DatabaseSync, logger: Logger | undefined): void {\n for (const statement of IDENTITY_STATEMENTS) {\n if (execUnless(sqlite, statement, toleratedFor(statement))) {\n // The connection opens with `returnArrays`: the count is the first value of the row.\n reportSkippedIndex(\n statement,\n (sql) => Number((sqlite.prepare(sql).get() as unknown as [number | bigint])[0]),\n logger,\n );\n }\n }\n sqlite.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\n}\n\nfunction runMigrations(sqlite: DatabaseSync, logger: Logger | undefined): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n applyIdentityStatements(sqlite, logger);\n dropWebhookSecret(sqlite);\n migrateBuildsGitShaIndex(sqlite);\n migrateCommentsTable(sqlite);\n}\n\n/** The columns of a table, from `PRAGMA table_info` (rows are arrays: the adapter opens with `returnArrays`). */\nfunction tableInfo(sqlite: DatabaseSync, table: string): TableColumns {\n return parseTableInfo(sqlite.prepare(`PRAGMA table_info(${table})`).all());\n}\n\n/** Drop the retired `webhooks.secret` column; a database that no longer has it is already migrated. */\nfunction dropWebhookSecret(sqlite: DatabaseSync): void {\n execUnless(sqlite, WEBHOOK_SECRET_DROP, isMissingColumn);\n}\n\n/** Downgrade a legacy UNIQUE project/sha index to a plain lookup index. */\nfunction migrateBuildsGitShaIndex(sqlite: DatabaseSync): void {\n sqlite.exec(BUILDS_GITSHA_DEDUP);\n sqlite.exec(BUILDS_GITSHA_INDEX);\n}\n"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ // src/statement-isolation.ts
5
+ /** The adapter methods that run one statement on the connection. */
6
+ const PLAIN_METHODS = [
7
+ "insert",
8
+ "update",
9
+ "updateWhere",
10
+ "get",
11
+ "remove",
12
+ "list",
13
+ "count",
14
+ "all"
15
+ ];
16
+ /**
17
+ * Make every plain method of `adapter` take a one-statement turn on `mutex`: after every
18
+ * transaction and statement that asked before it, so it never lands inside an open transaction
19
+ * (and is never rolled back with one), and an error at once from inside a callback, where it
20
+ * should have used `tx`. `health` takes a statement turn and `teardown` waits its turn, then closes.
21
+ */
22
+ function isolatePlainStatements(adapter, mutex) {
23
+ const wrapped = {};
24
+ for (const method of PLAIN_METHODS) {
25
+ const original = adapter[method];
26
+ if (original !== void 0) wrapped[method] = async (...args) => await mutex.runStatement(method, async () => await original.apply(adapter, args));
27
+ }
28
+ return {
29
+ ...adapter,
30
+ ...wrapped,
31
+ ...isolateLifecycle(adapter, mutex)
32
+ };
33
+ }
34
+ function isolateLifecycle(adapter, mutex) {
35
+ const { lifecycle } = adapter;
36
+ if (lifecycle === void 0) return {};
37
+ return { lifecycle: {
38
+ ...lifecycle,
39
+ setup: async (context) => {
40
+ await mutex.runStatement("setup", async () => {
41
+ await lifecycle.setup(context);
42
+ });
43
+ },
44
+ health: async () => await mutex.runStatement("health", async () => await lifecycle.health()),
45
+ teardown: async () => {
46
+ await mutex.runStatement("teardown", async () => {
47
+ await lifecycle.teardown();
48
+ });
49
+ }
50
+ } };
51
+ }
52
+ export { isolatePlainStatements };
53
+
54
+ //# sourceMappingURL=statement-isolation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statement-isolation.mjs","names":[],"sources":["../src/statement-isolation.ts"],"sourcesContent":["/**\n * Statement turns for the single-connection SQLite adapters: every plain method, `setup`, `health`\n * and `teardown` of an adapter takes its place in the queue its transactions use, so none lands\n * inside a transaction it does not belong to (see `transaction-mutex.ts`). Internal to the package.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { TransactionMutex } from \"./transaction-mutex.ts\";\n\ntype PlainMethod = (...args: unknown[]) => Promise<unknown>;\n\n/** The adapter methods that run one statement on the connection. */\nconst PLAIN_METHODS = [\"insert\", \"update\", \"updateWhere\", \"get\", \"remove\", \"list\", \"count\", \"all\"];\n\n/**\n * Make every plain method of `adapter` take a one-statement turn on `mutex`: after every\n * transaction and statement that asked before it, so it never lands inside an open transaction\n * (and is never rolled back with one), and an error at once from inside a callback, where it\n * should have used `tx`. `health` takes a statement turn and `teardown` waits its turn, then closes.\n */\nexport function isolatePlainStatements(\n adapter: DatabaseAdapter,\n mutex: TransactionMutex,\n): DatabaseAdapter {\n const wrapped: Record<string, unknown> = {};\n for (const method of PLAIN_METHODS) {\n // The one contained cast: the methods are generic, and this only forwards their arguments.\n const original = (adapter as unknown as Record<string, PlainMethod | undefined>)[method];\n if (original !== undefined) {\n wrapped[method] = async (...args: unknown[]): Promise<unknown> =>\n await mutex.runStatement(method, async () => await original.apply(adapter, args));\n }\n }\n return { ...adapter, ...wrapped, ...isolateLifecycle(adapter, mutex) };\n}\n\nfunction isolateLifecycle(\n adapter: DatabaseAdapter,\n mutex: TransactionMutex,\n): Pick<DatabaseAdapter, \"lifecycle\"> {\n const { lifecycle } = adapter;\n if (lifecycle === undefined) {\n return {};\n }\n return {\n lifecycle: {\n ...lifecycle,\n // Migrations run on the shared connection: never inside another caller's transaction.\n setup: async (context) => {\n await mutex.runStatement(\"setup\", async () => {\n await lifecycle.setup(context);\n });\n },\n health: async () => await mutex.runStatement(\"health\", async () => await lifecycle.health()),\n teardown: async () => {\n await mutex.runStatement(\"teardown\", async () => {\n await lifecycle.teardown();\n });\n },\n },\n };\n}\n"],"mappings":";;;;;AAWA,MAAM,gBAAgB;CAAC;CAAU;CAAU;CAAe;CAAO;CAAU;CAAQ;CAAS;AAAK;;;;;;;AAQjG,SAAgB,uBACd,SACA,OACiB;CACjB,MAAM,UAAmC,CAAC;CAC1C,KAAK,MAAM,UAAU,eAAe;EAElC,MAAM,WAAY,QAA+D;EACjF,IAAI,aAAa,KAAA,GACf,QAAQ,UAAU,OAAO,GAAG,SAC1B,MAAM,MAAM,aAAa,QAAQ,YAAY,MAAM,SAAS,MAAM,SAAS,IAAI,CAAC;CAEtF;CACA,OAAO;EAAE,GAAG;EAAS,GAAG;EAAS,GAAG,iBAAiB,SAAS,KAAK;CAAE;AACvE;AAEA,SAAS,iBACP,SACA,OACoC;CACpC,MAAM,EAAE,cAAc;CACtB,IAAI,cAAc,KAAA,GAChB,OAAO,CAAC;CAEV,OAAO,EACL,WAAW;EACT,GAAG;EAEH,OAAO,OAAO,YAAY;GACxB,MAAM,MAAM,aAAa,SAAS,YAAY;IAC5C,MAAM,UAAU,MAAM,OAAO;GAC/B,CAAC;EACH;EACA,QAAQ,YAAY,MAAM,MAAM,aAAa,UAAU,YAAY,MAAM,UAAU,OAAO,CAAC;EAC3F,UAAU,YAAY;GACpB,MAAM,MAAM,aAAa,YAAY,YAAY;IAC/C,MAAM,UAAU,SAAS;GAC3B,CAAC;EACH;CACF,EACF;AACF"}
@@ -19,24 +19,38 @@ const globalSchedule = (fire, ms) => {
19
19
  function createTransactionMutex(guard, options = {}) {
20
20
  const limit = options.waitLimitMs ?? (guard.detects ? void 0 : 3e4);
21
21
  let tail = Promise.resolve();
22
- return { run: async (body) => {
23
- if (guard.inside()) throw new Error("transact was called inside a transaction of the same database; use `tx`");
24
- const turn = new Turn();
22
+ /** Take the next place in the queue for `work`; `what` names the caller if its wait runs out. */
23
+ const queued = async (what, work) => {
24
+ const turn = new Turn(what);
25
25
  const done = tail.then(async () => {
26
26
  turn.begin();
27
- return await guard.enter(body);
27
+ return await work();
28
28
  });
29
29
  tail = done.catch(() => {});
30
30
  return await (limit === void 0 ? done : turn.within(done, limit, options.schedule ?? globalSchedule));
31
- } };
31
+ };
32
+ return {
33
+ run: async (body) => {
34
+ if (guard.inside()) throw new Error("transact was called inside a transaction of the same database; use `tx`");
35
+ return await queued("transaction", async () => await guard.enter(body));
36
+ },
37
+ runStatement: async (method, body) => {
38
+ if (guard.inside()) throw new Error(`a plain ${method} was called on the database inside its own transaction; use \`tx\``);
39
+ return await queued(`plain ${method}`, body);
40
+ }
41
+ };
32
42
  }
33
43
  /** One caller's place in the queue: whether its turn has come, and a limit on how long to wait for it. */
34
44
  var Turn = class {
35
45
  #started = false;
36
46
  #abandoned = false;
47
+ #what;
48
+ constructor(what) {
49
+ this.#what = what;
50
+ }
37
51
  /** The turn came: stop if the caller already gave up (the wait limit passed), else go on. */
38
52
  begin() {
39
- if (this.#abandoned) throw new Error("transaction gave up waiting for its turn");
53
+ if (this.#abandoned) throw new Error(`${this.#what} gave up waiting for its turn`);
40
54
  this.#started = true;
41
55
  }
42
56
  /** `done`, or an error if the turn has not come within `limitMs`. */
@@ -46,7 +60,7 @@ var Turn = class {
46
60
  cancel = schedule(() => {
47
61
  if (this.#started) return;
48
62
  this.#abandoned = true;
49
- reject(/* @__PURE__ */ new Error(`transaction waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \`tx\` instead`));
63
+ reject(/* @__PURE__ */ new Error(`${this.#what} waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \`tx\` instead`));
50
64
  }, limitMs);
51
65
  });
52
66
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-mutex.mjs","names":[],"sources":["../src/transaction-mutex.ts"],"sourcesContent":["/**\n * One transaction at a time per adapter. The built-in SQLite adapters run on a single connection,\n * and SQLite refuses a `BEGIN` inside an open transaction, so two overlapping `transact` calls\n * (the queue's acks, approvals under load) would fail the second one. The mutex makes the second\n * wait for the first to commit or roll back instead.\n *\n * A callback that calls `transact` on the same adapter again could never get the mutex it already\n * holds, which would hang, so every mutex takes a {@link NestingGuard} (`nesting-guard.ts`) that\n * turns that into an immediate error saying to use the `tx` the callback was given, where the\n * runtime can track async context.\n */\n\n/** Marks the code running inside a transaction, so a nested `transact` can be told apart. */\nexport interface NestingGuard {\n /** Whether the runtime can tell a nested call from an independent one at all. */\n detects: boolean;\n /** Run `body` as a transaction; `inside()` is true within it and anything it starts, until it ends. */\n enter<R>(body: () => Promise<R>): Promise<R>;\n /** Whether the caller runs inside a transaction that has not ended. */\n inside(): boolean;\n}\n\n/** Runs transaction bodies one at a time, in the order they asked. */\nexport interface TransactionMutex {\n run<R>(body: () => Promise<R>): Promise<R>;\n}\n\n/**\n * How long a transaction waits for its turn where nesting cannot be detected. A nested call there\n * waits for the transaction it sits in, which waits for it; the limit turns that into an error for\n * the nested call, which ends the outer transaction and frees the queue. It only counts the wait\n * for the turn, never a transaction's own run time.\n */\nexport const UNGUARDED_WAIT_LIMIT_MS = 30_000;\n\n/** Options of {@link createTransactionMutex}. */\nexport interface TransactionMutexOptions {\n /** Wait limit in ms; defaults to {@link UNGUARDED_WAIT_LIMIT_MS} when the guard cannot detect, else none. */\n waitLimitMs?: number;\n /** Runs `fire` after `ms` and returns what cancels it (default the global timers); tests inject a manual one. */\n schedule?: (fire: () => void, ms: number) => () => void;\n}\n\nconst globalSchedule = (fire: () => void, ms: number): (() => void) => {\n const timer = setTimeout(fire, ms);\n return () => {\n clearTimeout(timer);\n };\n};\n\n/** A mutex for one adapter; each adapter instance owns its own. */\nexport function createTransactionMutex(\n guard: NestingGuard,\n options: TransactionMutexOptions = {},\n): TransactionMutex {\n const limit = options.waitLimitMs ?? (guard.detects ? undefined : UNGUARDED_WAIT_LIMIT_MS);\n let tail: Promise<unknown> = Promise.resolve();\n return {\n run: async <R>(body: () => Promise<R>): Promise<R> => {\n if (guard.inside()) {\n throw new Error(\"transact was called inside a transaction of the same database; use `tx`\");\n }\n const turn = new Turn();\n const done = tail.then(async () => {\n turn.begin();\n return await guard.enter(body);\n });\n // The next transaction waits for this one to end either way, never for its error.\n tail = done.catch(() => {});\n return await (limit === undefined\n ? done\n : turn.within(done, limit, options.schedule ?? globalSchedule));\n },\n };\n}\n\n/** One caller's place in the queue: whether its turn has come, and a limit on how long to wait for it. */\nclass Turn {\n #started = false;\n #abandoned = false;\n\n /** The turn came: stop if the caller already gave up (the wait limit passed), else go on. */\n begin(): void {\n if (this.#abandoned) {\n throw new Error(\"transaction gave up waiting for its turn\");\n }\n this.#started = true;\n }\n\n /** `done`, or an error if the turn has not come within `limitMs`. */\n async within<R>(\n done: Promise<R>,\n limitMs: number,\n schedule: (fire: () => void, ms: number) => () => void,\n ): Promise<R> {\n let cancel: (() => void) | undefined;\n const expired = new Promise<never>((_resolve, reject) => {\n cancel = schedule(() => {\n if (this.#started) return;\n this.#abandoned = true;\n reject(\n new Error(\n `transaction waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \\`tx\\` instead`,\n ),\n );\n }, limitMs);\n });\n try {\n return await Promise.race([done, expired]);\n } finally {\n cancel?.();\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,MAAa,0BAA0B;AAUvC,MAAM,kBAAkB,MAAkB,OAA6B;CACrE,MAAM,QAAQ,WAAW,MAAM,EAAE;CACjC,aAAa;EACX,aAAa,KAAK;CACpB;AACF;;AAGA,SAAgB,uBACd,OACA,UAAmC,CAAC,GAClB;CAClB,MAAM,QAAQ,QAAQ,gBAAgB,MAAM,UAAU,KAAA,IAAA;CACtD,IAAI,OAAyB,QAAQ,QAAQ;CAC7C,OAAO,EACL,KAAK,OAAU,SAAuC;EACpD,IAAI,MAAM,OAAO,GACf,MAAM,IAAI,MAAM,yEAAyE;EAE3F,MAAM,OAAO,IAAI,KAAK;EACtB,MAAM,OAAO,KAAK,KAAK,YAAY;GACjC,KAAK,MAAM;GACX,OAAO,MAAM,MAAM,MAAM,IAAI;EAC/B,CAAC;EAED,OAAO,KAAK,YAAY,CAAC,CAAC;EAC1B,OAAO,OAAO,UAAU,KAAA,IACpB,OACA,KAAK,OAAO,MAAM,OAAO,QAAQ,YAAY,cAAc;CACjE,EACF;AACF;;AAGA,IAAM,OAAN,MAAW;CACT,WAAW;CACX,aAAa;;CAGb,QAAc;EACZ,IAAI,KAAK,YACP,MAAM,IAAI,MAAM,0CAA0C;EAE5D,KAAK,WAAW;CAClB;;CAGA,MAAM,OACJ,MACA,SACA,UACY;EACZ,IAAI;EACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;GACvD,SAAS,eAAe;IACtB,IAAI,KAAK,UAAU;IACnB,KAAK,aAAa;IAClB,uBACE,IAAI,MACF,sBAAsB,OAAO,OAAO,EAAE,gHACxC,CACF;GACF,GAAG,OAAO;EACZ,CAAC;EACD,IAAI;GACF,OAAO,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;EAC3C,UAAU;GACR,SAAS;EACX;CACF;AACF"}
1
+ {"version":3,"file":"transaction-mutex.mjs","names":[],"sources":["../src/transaction-mutex.ts"],"sourcesContent":["/**\n * One transaction at a time per adapter. The built-in SQLite adapters run on a single connection,\n * and SQLite refuses a `BEGIN` inside an open transaction, so two overlapping `transact` calls\n * (the queue's acks, approvals under load) would fail the second one. The mutex makes the second\n * wait for the first to commit or roll back instead.\n *\n * A callback that calls `transact` on the same adapter again could never get the mutex it already\n * holds, which would hang, so every mutex takes a {@link NestingGuard} (`nesting-guard.ts`) that\n * turns that into an immediate error saying to use the `tx` the callback was given, where the\n * runtime can track async context.\n *\n * A plain statement (an `insert`, a page's `get`) needs the same care: on the one connection it\n * would run inside whatever transaction is open and be committed or rolled back with it. It takes\n * a one-statement turn on the same queue, so it can never land inside a transaction, and one\n * issued on the outer adapter from inside a callback is the same misuse as a nested `transact`.\n * Only the built-in adapters on a single connection (`node:sqlite`, better-sqlite3, `bun:sqlite`)\n * use this; Turso, D1, Postgres and MySQL have real transaction handles or pooled connections.\n */\n\n/** Marks the code running inside a transaction, so a nested `transact` can be told apart. */\nexport interface NestingGuard {\n /** Whether the runtime can tell a nested call from an independent one at all. */\n detects: boolean;\n /** Run `body` as a transaction; `inside()` is true within it and anything it starts, until it ends. */\n enter<R>(body: () => Promise<R>): Promise<R>;\n /** Whether the caller runs inside a transaction that has not ended. */\n inside(): boolean;\n}\n\n/** Runs transaction bodies one at a time, in the order they asked. */\nexport interface TransactionMutex {\n run<R>(body: () => Promise<R>): Promise<R>;\n /**\n * Run one plain statement (`method` names it in errors) as a turn of its own: after every\n * transaction and statement that asked before it, never inside one. Throws at once from inside a\n * transaction's own async context, where waiting would never end.\n */\n runStatement<R>(method: string, body: () => Promise<R>): Promise<R>;\n}\n\n/**\n * How long a transaction waits for its turn where nesting cannot be detected. A nested call there\n * waits for the transaction it sits in, which waits for it; the limit turns that into an error for\n * the nested call, which ends the outer transaction and frees the queue. It only counts the wait\n * for the turn, never a transaction's own run time.\n */\nexport const UNGUARDED_WAIT_LIMIT_MS = 30_000;\n\n/** Options of {@link createTransactionMutex}. */\nexport interface TransactionMutexOptions {\n /** Wait limit in ms; defaults to {@link UNGUARDED_WAIT_LIMIT_MS} when the guard cannot detect, else none. */\n waitLimitMs?: number;\n /** Runs `fire` after `ms` and returns what cancels it (default the global timers); tests inject a manual one. */\n schedule?: (fire: () => void, ms: number) => () => void;\n}\n\nconst globalSchedule = (fire: () => void, ms: number): (() => void) => {\n const timer = setTimeout(fire, ms);\n return () => {\n clearTimeout(timer);\n };\n};\n\n/** A mutex for one adapter; each adapter instance owns its own. */\nexport function createTransactionMutex(\n guard: NestingGuard,\n options: TransactionMutexOptions = {},\n): TransactionMutex {\n const limit = options.waitLimitMs ?? (guard.detects ? undefined : UNGUARDED_WAIT_LIMIT_MS);\n let tail: Promise<unknown> = Promise.resolve();\n /** Take the next place in the queue for `work`; `what` names the caller if its wait runs out. */\n const queued = async <R>(what: string, work: () => Promise<R>): Promise<R> => {\n const turn = new Turn(what);\n const done = tail.then(async () => {\n turn.begin();\n return await work();\n });\n // The next turn waits for this one to end either way, never for its error.\n tail = done.catch(() => {});\n return await (limit === undefined\n ? done\n : turn.within(done, limit, options.schedule ?? globalSchedule));\n };\n return {\n run: async <R>(body: () => Promise<R>): Promise<R> => {\n if (guard.inside()) {\n throw new Error(\"transact was called inside a transaction of the same database; use `tx`\");\n }\n return await queued(\"transaction\", async () => await guard.enter(body));\n },\n runStatement: async <R>(method: string, body: () => Promise<R>): Promise<R> => {\n if (guard.inside()) {\n throw new Error(\n `a plain ${method} was called on the database inside its own transaction; use \\`tx\\``,\n );\n }\n return await queued(`plain ${method}`, body);\n },\n };\n}\n\n/** One caller's place in the queue: whether its turn has come, and a limit on how long to wait for it. */\nclass Turn {\n #started = false;\n #abandoned = false;\n readonly #what: string;\n\n constructor(what: string) {\n this.#what = what;\n }\n\n /** The turn came: stop if the caller already gave up (the wait limit passed), else go on. */\n begin(): void {\n if (this.#abandoned) {\n throw new Error(`${this.#what} gave up waiting for its turn`);\n }\n this.#started = true;\n }\n\n /** `done`, or an error if the turn has not come within `limitMs`. */\n async within<R>(\n done: Promise<R>,\n limitMs: number,\n schedule: (fire: () => void, ms: number) => () => void,\n ): Promise<R> {\n let cancel: (() => void) | undefined;\n const expired = new Promise<never>((_resolve, reject) => {\n cancel = schedule(() => {\n if (this.#started) return;\n this.#abandoned = true;\n reject(\n new Error(\n `${this.#what} waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \\`tx\\` instead`,\n ),\n );\n }, limitMs);\n });\n try {\n return await Promise.race([done, expired]);\n } finally {\n cancel?.();\n }\n }\n}\n"],"mappings":";;;;;;;;;;AA8CA,MAAa,0BAA0B;AAUvC,MAAM,kBAAkB,MAAkB,OAA6B;CACrE,MAAM,QAAQ,WAAW,MAAM,EAAE;CACjC,aAAa;EACX,aAAa,KAAK;CACpB;AACF;;AAGA,SAAgB,uBACd,OACA,UAAmC,CAAC,GAClB;CAClB,MAAM,QAAQ,QAAQ,gBAAgB,MAAM,UAAU,KAAA,IAAA;CACtD,IAAI,OAAyB,QAAQ,QAAQ;;CAE7C,MAAM,SAAS,OAAU,MAAc,SAAuC;EAC5E,MAAM,OAAO,IAAI,KAAK,IAAI;EAC1B,MAAM,OAAO,KAAK,KAAK,YAAY;GACjC,KAAK,MAAM;GACX,OAAO,MAAM,KAAK;EACpB,CAAC;EAED,OAAO,KAAK,YAAY,CAAC,CAAC;EAC1B,OAAO,OAAO,UAAU,KAAA,IACpB,OACA,KAAK,OAAO,MAAM,OAAO,QAAQ,YAAY,cAAc;CACjE;CACA,OAAO;EACL,KAAK,OAAU,SAAuC;GACpD,IAAI,MAAM,OAAO,GACf,MAAM,IAAI,MAAM,yEAAyE;GAE3F,OAAO,MAAM,OAAO,eAAe,YAAY,MAAM,MAAM,MAAM,IAAI,CAAC;EACxE;EACA,cAAc,OAAU,QAAgB,SAAuC;GAC7E,IAAI,MAAM,OAAO,GACf,MAAM,IAAI,MACR,WAAW,OAAO,mEACpB;GAEF,OAAO,MAAM,OAAO,SAAS,UAAU,IAAI;EAC7C;CACF;AACF;;AAGA,IAAM,OAAN,MAAW;CACT,WAAW;CACX,aAAa;CACb;CAEA,YAAY,MAAc;EACxB,KAAK,QAAQ;CACf;;CAGA,QAAc;EACZ,IAAI,KAAK,YACP,MAAM,IAAI,MAAM,GAAG,KAAK,MAAM,8BAA8B;EAE9D,KAAK,WAAW;CAClB;;CAGA,MAAM,OACJ,MACA,SACA,UACY;EACZ,IAAI;EACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;GACvD,SAAS,eAAe;IACtB,IAAI,KAAK,UAAU;IACnB,KAAK,aAAa;IAClB,uBACE,IAAI,MACF,GAAG,KAAK,MAAM,UAAU,OAAO,OAAO,EAAE,gHAC1C,CACF;GACF,GAAG,OAAO;EACZ,CAAC;EACD,IAAI;GACF,OAAO,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;EAC3C,UAAU;GACR,SAAS;EACX;CACF;AACF"}
package/dist/turso.mjs CHANGED
@@ -34,7 +34,7 @@ function createTursoDatabase(options) {
34
34
  return createDrizzleAdapter(db, {
35
35
  metadata: {
36
36
  name: "Turso",
37
- version: "0.10.0-next-1e4feb2",
37
+ version: "0.10.0-next-75a95bc",
38
38
  description: "Turso/libSQL database adapter",
39
39
  kind: "turso",
40
40
  category: "database"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/db-sqlite",
3
- "version": "0.10.0-next-1e4feb2",
3
+ "version": "0.10.0-next-75a95bc",
4
4
  "description": "SQLite database adapter for StoryShelf (node:sqlite + Drizzle).",
5
5
  "keywords": [
6
6
  "database",
@@ -87,7 +87,7 @@
87
87
  "prepublishOnly": "nub run build"
88
88
  },
89
89
  "dependencies": {
90
- "@storyshelf/core": "0.10.0-next-1e4feb2",
90
+ "@storyshelf/core": "0.10.0-next-75a95bc",
91
91
  "drizzle-orm": "^0.45.4"
92
92
  },
93
93
  "devDependencies": {