@morlay/rdbkit 0.0.0-stage → 0.0.1-alpha.3

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/dist/index.d.mts +518 -0
  4. package/dist/index.mjs +1422 -0
  5. package/dist/testing.d.mts +28 -0
  6. package/dist/testing.mjs +42 -0
  7. package/package.json +49 -3
  8. package/src/__tests__/database-introspect.spec.ts +71 -0
  9. package/src/__tests__/expr-functions.spec.ts +64 -0
  10. package/src/__tests__/migration.spec.ts +81 -0
  11. package/src/__tests__/pg.spec.ts +123 -0
  12. package/src/__tests__/query-join.spec.ts +114 -0
  13. package/src/__tests__/query-p1.spec.ts +108 -0
  14. package/src/__tests__/query-write.spec.ts +293 -0
  15. package/src/__tests__/query.spec.ts +88 -0
  16. package/src/__tests__/schema.spec.ts +144 -0
  17. package/src/__tests__/transaction.spec.ts +83 -0
  18. package/src/catalog.ts +24 -0
  19. package/src/compound.ts +64 -0
  20. package/src/database/capabilities.ts +70 -0
  21. package/src/database/create.ts +119 -0
  22. package/src/database/index.ts +9 -0
  23. package/src/database/postgres.ts +133 -0
  24. package/src/database/queue.ts +24 -0
  25. package/src/database/sqlite.ts +66 -0
  26. package/src/database/types.ts +87 -0
  27. package/src/database/wrap.ts +48 -0
  28. package/src/defs.ts +82 -0
  29. package/src/dialect.ts +219 -0
  30. package/src/error.ts +72 -0
  31. package/src/expr.ts +346 -0
  32. package/src/fragment.ts +96 -0
  33. package/src/index.ts +114 -0
  34. package/src/migrate.ts +133 -0
  35. package/src/migration.ts +82 -0
  36. package/src/table/handle.ts +142 -0
  37. package/src/table/index.ts +14 -0
  38. package/src/table/render.ts +140 -0
  39. package/src/table/select.ts +229 -0
  40. package/src/table/types.ts +53 -0
  41. package/src/testing.ts +56 -0
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { count, defineTable, E, ensureSchema, openDatabase } from "../index.ts";
3
+
4
+ const rows = defineTable({
5
+ name: "t_tx_rows",
6
+ columns: {
7
+ f_id: { type: "text", primaryKey: true, notNull: true },
8
+ f_value: { type: "integer", notNull: true, default: 0 },
9
+ },
10
+ });
11
+
12
+ async function freshDatabase(): Promise<Awaited<ReturnType<typeof openDatabase>>> {
13
+ const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
14
+ await ensureSchema(database, [rows]);
15
+ return database;
16
+ }
17
+
18
+ async function countRows(
19
+ database: Awaited<ReturnType<typeof openDatabase>>,
20
+ ): Promise<number> {
21
+ const table = database.table(rows);
22
+ let total = 0;
23
+ for await (const row of table.select({ n: count() })) total = Number(row.n);
24
+ return total;
25
+ }
26
+
27
+ describe("事务通道", () => {
28
+ it("并发事务各自完整:不出现「事务中再开事务」", async () => {
29
+ const database = await freshDatabase();
30
+ try {
31
+ const table = database.table(rows);
32
+ await Promise.all(
33
+ Array.from({ length: 20 }, (_, index) =>
34
+ database.transaction(async (tx) => {
35
+ await tx.table(rows).insert({ f_id: `row-${index}`, f_value: index });
36
+ }),
37
+ ),
38
+ );
39
+ expect(await countRows(database)).toBe(20);
40
+ expect(table.name).toBe("t_tx_rows");
41
+ } finally {
42
+ await database.close();
43
+ }
44
+ });
45
+
46
+ it("事务内再开事务走保存点:内层失败只回滚内层", async () => {
47
+ const database = await freshDatabase();
48
+ try {
49
+ await database.transaction(async (tx) => {
50
+ await tx.table(rows).insert({ f_id: "outer", f_value: 1 });
51
+ await expect(
52
+ database.transaction(async (inner) => {
53
+ await inner.table(rows).insert({ f_id: "inner", f_value: 2 });
54
+ throw new Error("inner failed");
55
+ }),
56
+ ).rejects.toThrow("inner failed");
57
+ await tx.table(rows).insert({ f_id: "outer-2", f_value: 3 });
58
+ });
59
+ const ids: string[] = [];
60
+ for await (const row of database.table(rows).select({ id: E.column("f_id") }).orderBy("f_id")) {
61
+ ids.push(row.id);
62
+ }
63
+ expect(ids).toEqual(["outer", "outer-2"]);
64
+ } finally {
65
+ await database.close();
66
+ }
67
+ });
68
+
69
+ it("事务内的写与事务同生共死", async () => {
70
+ const database = await freshDatabase();
71
+ try {
72
+ await expect(
73
+ database.transaction(async (tx) => {
74
+ await tx.table(rows).insert({ f_id: "rolled-back", f_value: 1 });
75
+ throw new Error("boom");
76
+ }),
77
+ ).rejects.toThrow("boom");
78
+ expect(await countRows(database)).toBe(0);
79
+ } finally {
80
+ await database.close();
81
+ }
82
+ });
83
+ });
package/src/catalog.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Catalog, TableDef } from "./defs.ts";
2
+
3
+ /** 一个按表名解析的 catalog(装配层注册一次,业务按名字取表)。 */
4
+ export interface CatalogIndex {
5
+ get(name: string): TableDef | undefined;
6
+ has(name: string): boolean;
7
+ names(): string[];
8
+ }
9
+
10
+ /** 把表定义列表编成索引(重复表名 fail loud)。 */
11
+ export function indexCatalog(catalog: Catalog): CatalogIndex {
12
+ const tables = new Map<string, TableDef>();
13
+ for (const table of catalog) {
14
+ if (tables.has(table.name)) {
15
+ throw new Error(`rdbkit: duplicate table '${table.name}' in catalog`);
16
+ }
17
+ tables.set(table.name, table);
18
+ }
19
+ return {
20
+ get: (name) => tables.get(name),
21
+ has: (name) => tables.has(name),
22
+ names: () => [...tables.keys()],
23
+ };
24
+ }
@@ -0,0 +1,64 @@
1
+ import { concat, text, type Fragment } from "./fragment.ts";
2
+ import type { TableDef } from "./defs.ts";
3
+
4
+ /** 一条 CTE:`"name" AS (<query>)`;`recursive` 时由 `withCtes` 渲染成 `WITH RECURSIVE` 前缀。 */
5
+ export interface Cte {
6
+ readonly name: string;
7
+ readonly query: Fragment;
8
+ readonly recursive?: boolean;
9
+ readonly columns?: readonly string[];
10
+ }
11
+
12
+ /** 把一条 CTE 拼在 `WITH` 之后(调用方负责逗号)。 */
13
+ export function cteSql(cte: Cte): Fragment {
14
+ const columns =
15
+ cte.columns === undefined ? "" : ` (${cte.columns.map((name) => `"${name}"`).join(", ")})`;
16
+ return concat([text(`"${cte.name}"${columns} AS (`), cte.query, text(")")], " ");
17
+ }
18
+
19
+ /** 组装 `WITH [RECURSIVE] cte, … SELECT …`。 */
20
+ export function withCtes(ctes: readonly Cte[], query: Fragment): Fragment {
21
+ if (ctes.length === 0) return query;
22
+ const recursive = ctes.some((cte) => cte.recursive === true);
23
+ return concat(
24
+ [text(recursive ? "WITH RECURSIVE" : "WITH"), concat(ctes.map(cteSql), ", "), query],
25
+ " ",
26
+ );
27
+ }
28
+
29
+ /** `UNION` / `UNION ALL`。 */
30
+ export function union(left: Fragment, right: Fragment, options: { all?: boolean } = {}): Fragment {
31
+ return concat([left, text(options.all === true ? "UNION ALL" : "UNION"), right], " ");
32
+ }
33
+
34
+ /** `INTERSECT`。 */
35
+ export function intersect(left: Fragment, right: Fragment): Fragment {
36
+ return concat([left, text("INTERSECT"), right], " ");
37
+ }
38
+
39
+ /** `EXCEPT`。 */
40
+ export function except(left: Fragment, right: Fragment): Fragment {
41
+ return concat([left, text("EXCEPT"), right], " ");
42
+ }
43
+
44
+ /** 行锁:只有 PostgreSQL 认,SQLite 渲染时整段丢掉(分隔符也不能漏出来)。 */
45
+ export function forUpdate(
46
+ options: {
47
+ of?: readonly string[];
48
+ noWait?: boolean;
49
+ skipLocked?: boolean;
50
+ } = {},
51
+ ): Fragment {
52
+ const parts = ["FOR UPDATE"];
53
+ if (options.of !== undefined && options.of.length > 0) {
54
+ parts.push(`OF ${options.of.map((name) => `"${name}"`).join(", ")}`);
55
+ }
56
+ if (options.noWait === true) parts.push("NOWAIT");
57
+ if (options.skipLocked === true) parts.push("SKIP LOCKED");
58
+ return text(parts.join(" "), { only: "postgres" });
59
+ }
60
+
61
+ /** 表别名(join / CTE 里用)。 */
62
+ export function aliasedTable(table: TableDef, alias: string): string {
63
+ return `"${table.name}" AS "${alias}"`;
64
+ }
@@ -0,0 +1,70 @@
1
+ import { fragment } from "../fragment.ts";
2
+ import type { Database, SqlitePragmaName } from "./types.ts";
3
+
4
+ function assertPragmaName(name: SqlitePragmaName): void {
5
+ if (name !== "user_version" && name !== "application_id" && name !== "journal_mode") {
6
+ throw new Error(`rdbkit: unsupported pragma '${String(name)}'`);
7
+ }
8
+ }
9
+
10
+ /** PRAGMA 不支持绑定参数,值只能内联:只放行整数与标识符形状的字符串。 */
11
+ function assertPragmaValue(name: SqlitePragmaName, value: string | number): string {
12
+ if (typeof value === "number") {
13
+ if (!Number.isSafeInteger(value)) {
14
+ throw new Error(`rdbkit: invalid pragma value '${String(value)}' for '${name}'`);
15
+ }
16
+ return String(value);
17
+ }
18
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(value)) {
19
+ throw new Error(`rdbkit: invalid pragma value '${value}' for '${name}'`);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ /** 现有的表与视图名(迁移探测旧表、拒绝别人的库都要它)。 */
25
+ export async function objectNames(database: Database): Promise<string[]> {
26
+ const query =
27
+ database.dialect.name === "sqlite"
28
+ ? fragment(
29
+ "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'",
30
+ )
31
+ : fragment(
32
+ "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()",
33
+ );
34
+ const rows = await database.read<{ name: string }>(query);
35
+ return rows.map((row) => row.name);
36
+ }
37
+
38
+ /** 回收空间与统计(SQLite `VACUUM` / PostgreSQL `VACUUM ANALYZE`);不得在事务内调用。 */
39
+ export async function vacuum(database: Database): Promise<void> {
40
+ await database.run(
41
+ fragment(database.dialect.name === "sqlite" ? "VACUUM" : "VACUUM ANALYZE"),
42
+ );
43
+ }
44
+
45
+ /** 读 SQLite 的 `PRAGMA`(其它方言直接报不支持)。 */
46
+ export async function sqlitePragma(
47
+ database: Database,
48
+ name: SqlitePragmaName,
49
+ ): Promise<string | number | undefined> {
50
+ assertPragmaName(name);
51
+ if (database.dialect.name !== "sqlite") {
52
+ throw new Error(`rdbkit: '${name}' is a SQLite PRAGMA; this database is postgres`);
53
+ }
54
+ const rows = await database.read<Record<string, unknown>>(fragment(`PRAGMA ${name}`));
55
+ const value = rows[0]?.[name];
56
+ return typeof value === "number" || typeof value === "string" ? value : undefined;
57
+ }
58
+
59
+ /** 写 SQLite 的 `PRAGMA`(值只允许安全字面量,见 `assertPragmaValue`)。 */
60
+ export async function setSqlitePragma(
61
+ database: Database,
62
+ name: SqlitePragmaName,
63
+ value: string | number,
64
+ ): Promise<void> {
65
+ assertPragmaName(name);
66
+ if (database.dialect.name !== "sqlite") {
67
+ throw new Error(`rdbkit: '${name}' is a SQLite PRAGMA; this database is postgres`);
68
+ }
69
+ await database.run(fragment(`PRAGMA ${name} = ${assertPragmaValue(name, value)}`));
70
+ }
@@ -0,0 +1,119 @@
1
+ import type { Catalog, TableDef } from "../defs.ts";
2
+ import { createDialect, type Dialect } from "../dialect.ts";
3
+ import { render, type Fragment } from "../fragment.ts";
4
+ import { indexCatalog, type CatalogIndex } from "../catalog.ts";
5
+ import { createTableHandle } from "../table/handle.ts";
6
+ import { classifyError } from "../error.ts";
7
+ import { objectNames, setSqlitePragma, sqlitePragma, vacuum } from "./capabilities.ts";
8
+ import { openPostgres } from "./postgres.ts";
9
+ import { openSqlite } from "./sqlite.ts";
10
+ import type { Database, Executor, OpenConfigWithCatalog, QueryEvent } from "./types.ts";
11
+
12
+ /** 打开时的 catalog:按表名建一次索引(之后 `table("名字")` 才解析得到)。 */
13
+ function withCatalog<C extends Catalog>(config: OpenConfigWithCatalog<C>): CatalogIndex {
14
+ return indexCatalog(config.catalog ?? []);
15
+ }
16
+
17
+ /** 打开一个库;这是连接创建的唯一入口(实际 connect 在 `sqlite.ts` / `postgres.ts`,业务只拿 `Database`)。 */
18
+ export async function openDatabase<const C extends Catalog = Catalog>(
19
+ config: OpenConfigWithCatalog<C>,
20
+ ): Promise<Database<C>> {
21
+ const catalog = withCatalog(config);
22
+ const executor =
23
+ config.dialect === "sqlite" ? openSqlite(config) : openPostgres(config);
24
+ return createDatabase(
25
+ createDialect(config.dialect),
26
+ executor,
27
+ catalog,
28
+ config.onQuery,
29
+ ) as Database<C>;
30
+ }
31
+
32
+ async function* iterateWrapped<T>(
33
+ rows: AsyncIterable<unknown>,
34
+ dialect: "sqlite" | "postgres",
35
+ ): AsyncIterable<T> {
36
+ try {
37
+ for await (const row of rows) yield row as T;
38
+ } catch (error) {
39
+ throw classifyError(error, dialect);
40
+ }
41
+ }
42
+
43
+ export function createDatabase(
44
+ dialect: Dialect,
45
+ executor: Executor,
46
+ catalog: CatalogIndex,
47
+ onQuery?: (event: QueryEvent) => void,
48
+ ): Database {
49
+ const observe = async <T>(
50
+ query: Fragment,
51
+ run: (text: string, params: readonly unknown[]) => Promise<T>,
52
+ ): Promise<T> => {
53
+ const { text, params } = render(query, dialect.name);
54
+ const started = performance.now();
55
+ try {
56
+ const result = await run(text, params);
57
+ onQuery?.({
58
+ dialect: dialect.name,
59
+ sql: text,
60
+ params,
61
+ durationMs: performance.now() - started,
62
+ });
63
+ return result;
64
+ } catch (error) {
65
+ onQuery?.({
66
+ dialect: dialect.name,
67
+ sql: text,
68
+ params,
69
+ durationMs: performance.now() - started,
70
+ error,
71
+ });
72
+ throw classifyError(error, dialect.name);
73
+ }
74
+ };
75
+ const database: Database = {
76
+ dialect,
77
+ catalog,
78
+ table: (source: TableDef | string) => {
79
+ const definition = typeof source === "string" ? catalog.get(source) : source;
80
+ if (definition === undefined) {
81
+ throw new Error(
82
+ `rdbkit: no table '${typeof source === "string" ? source : source.name}' in catalog`,
83
+ );
84
+ }
85
+ return createTableHandle(database, definition) as never;
86
+ },
87
+ iterate<T>(query: Fragment) {
88
+ const { text, params } = render(query, dialect.name);
89
+ return iterateWrapped<T>(executor.iterate(text, params), dialect.name);
90
+ },
91
+ read<T>(query: Fragment) {
92
+ return observe(query, async () => {
93
+ const rows: T[] = [];
94
+ for await (const row of database.iterate<T>(query)) rows.push(row);
95
+ return rows;
96
+ });
97
+ },
98
+ write<T>(query: Fragment) {
99
+ return database.read<T>(query);
100
+ },
101
+ run(query) {
102
+ return observe(query, (text, params) => executor.run(text, params));
103
+ },
104
+ transaction(fn) {
105
+ // catalog 跟着事务走:事务里按表名取句柄与外面一致。
106
+ return executor.transaction((scoped) =>
107
+ fn(createDatabase(dialect, scoped, catalog, onQuery)),
108
+ );
109
+ },
110
+ objectNames: () => objectNames(database),
111
+ vacuum: () => vacuum(database),
112
+ sqlitePragma: (name) => sqlitePragma(database, name),
113
+ setSqlitePragma: (name, value) => setSqlitePragma(database, name, value),
114
+ async close() {
115
+ await executor.close();
116
+ },
117
+ };
118
+ return database;
119
+ }
@@ -0,0 +1,9 @@
1
+ export { createDatabase, openDatabase } from "./create.ts";
2
+ export { wrapDatabase, shapeReader } from "./wrap.ts";
3
+ export type {
4
+ Database,
5
+ OpenConfig,
6
+ OpenConfigWithCatalog,
7
+ QueryEvent,
8
+ SqlitePragmaName,
9
+ } from "./types.ts";
@@ -0,0 +1,133 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { Pool, types as pgTypes, type PoolClient } from "pg";
3
+ import Cursor from "pg-cursor";
4
+ import { createSerialQueue, savepointName } from "./queue.ts";
5
+ import type { Executor, OpenConfig } from "./types.ts";
6
+
7
+ /**
8
+ * PostgreSQL 的 `bigint`(int8)与 `numeric` 默认由驱动给字符串。
9
+ *
10
+ * 值是 64 位整数时**不能用 Number 兜**(超过 `2^53` 就悄悄丢精度,`uint64` 的 ID 会撞车),所以:
11
+ * - `int8`(OID 20)→ JS `BigInt`(精确,对应 Go 的 int64 / uint64);
12
+ * - `numeric`(OID 1700)保持驱动默认的字符串(可能是定点小数,BigInt 也不对)。
13
+ *
14
+ * 业务侧要 number 时自己 `Number(x)`(毫秒时间戳与计数在安全整数内,转换是安全的)。
15
+ */
16
+ export function bigintTypes(): {
17
+ getTypeParser: (oid: number, format?: "text" | "binary") => (value: string) => unknown;
18
+ } {
19
+ const INT8 = 20;
20
+ return {
21
+ getTypeParser(oid, format) {
22
+ if (oid === INT8) {
23
+ return (value: string) => (value === null ? null : BigInt(value));
24
+ }
25
+ const parser = pgTypes.getTypeParser(oid, format);
26
+ return parser as (value: string) => unknown;
27
+ },
28
+ };
29
+ }
30
+
31
+ /** PostgreSQL 连接池:返回池上的执行面(游标 + int8 → BigInt)。 */
32
+ export function openPostgres(config: Extract<OpenConfig, { dialect: "postgres" }>): Executor {
33
+ const pool = new Pool({ connectionString: config.connectionString, types: bigintTypes() });
34
+ const enqueue = createSerialQueue();
35
+ // 当前异步上下文所在的事务连接(嵌套时共享同一条连接,用保存点)。
36
+ const current = new AsyncLocalStorage<PoolClient>();
37
+
38
+ // 真游标:按 200 行一批从连接读,调用方提前 break 时释放。
39
+ const cursorRows = async function* (
40
+ client: PoolClient,
41
+ text: string,
42
+ params: readonly unknown[],
43
+ ): AsyncIterable<unknown> {
44
+ const cursor = client.query(new Cursor(text, params as never[]));
45
+ try {
46
+ for (;;) {
47
+ const rows = (await cursor.read(200)) as unknown[];
48
+ if (rows.length === 0) return;
49
+ for (const row of rows) yield row;
50
+ }
51
+ } finally {
52
+ await cursor.close().catch(() => undefined);
53
+ }
54
+ };
55
+
56
+ const pooledIterate = async function* (
57
+ text: string,
58
+ params: readonly unknown[],
59
+ ): AsyncIterable<unknown> {
60
+ const client = await pool.connect();
61
+ try {
62
+ yield* cursorRows(client, text, params);
63
+ } finally {
64
+ client.release();
65
+ }
66
+ };
67
+
68
+ /** 一条连接上的执行面(顶层事务与嵌套事务共用)。 */
69
+ const scopeOf = (client: PoolClient): Executor => ({
70
+ iterate: (text, params) => cursorRows(client, text, params),
71
+ async run(text, params) {
72
+ await client.query(text, params as never[]);
73
+ },
74
+ transaction(fn) {
75
+ const name = savepointName();
76
+ return (async () => {
77
+ await client.query(`SAVEPOINT ${name}`);
78
+ try {
79
+ const result = await current.run(client, () => fn(scopeOf(client)));
80
+ await client.query(`RELEASE SAVEPOINT ${name}`);
81
+ return result;
82
+ } catch (error) {
83
+ await client.query(`ROLLBACK TO SAVEPOINT ${name}`);
84
+ await client.query(`RELEASE SAVEPOINT ${name}`);
85
+ throw error;
86
+ }
87
+ })();
88
+ },
89
+ async close() {
90
+ // 连接由顶层事务释放,这里什么都不做。
91
+ },
92
+ });
93
+
94
+ const executor: Executor = {
95
+ iterate: pooledIterate,
96
+ run(text, params) {
97
+ const client = current.getStore();
98
+ if (client !== undefined) {
99
+ return client.query(text, params as never[]).then(() => undefined);
100
+ }
101
+ return enqueue(async () => {
102
+ await pool.query(text, params as never[]);
103
+ });
104
+ },
105
+ transaction(fn) {
106
+ const held = current.getStore();
107
+ if (held !== undefined) return scopeOf(held).transaction(fn);
108
+ return enqueue(async () => {
109
+ const client = await pool.connect();
110
+ try {
111
+ await client.query(
112
+ config.isolationLevel === undefined
113
+ ? "BEGIN"
114
+ : `BEGIN ISOLATION LEVEL ${config.isolationLevel.toUpperCase()}`,
115
+ );
116
+ const result = await current.run(client, () => fn(scopeOf(client)));
117
+ await client.query("COMMIT");
118
+ return result;
119
+ } catch (error) {
120
+ await client.query("ROLLBACK");
121
+ throw error;
122
+ } finally {
123
+ client.release();
124
+ }
125
+ });
126
+ },
127
+ async close() {
128
+ await enqueue(async () => undefined);
129
+ await pool.end();
130
+ },
131
+ };
132
+ return executor;
133
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 写通道串行:同一实例的事务与写语句排队执行(SQLite 是单连接,PG 的 serializable 并发会冲突)。
3
+ * 「当前异步上下文是否已在事务里」由方言层用 `AsyncLocalStorage` 判断(`sqlite.ts` / `postgres.ts`),
4
+ * 这里只提供队列本身。
5
+ */
6
+ export function createSerialQueue(): <T>(fn: () => Promise<T>) => Promise<T> {
7
+ let tail: Promise<unknown> = Promise.resolve();
8
+ return <T>(fn: () => Promise<T>): Promise<T> => {
9
+ const next = tail.then(fn, fn);
10
+ tail = next.then(
11
+ () => undefined,
12
+ () => undefined,
13
+ );
14
+ return next;
15
+ };
16
+ }
17
+
18
+ /** 保存点名:只在同一个事务里唯一即可。 */
19
+ let savepointCounter = 0;
20
+
21
+ export function savepointName(): string {
22
+ savepointCounter += 1;
23
+ return `rdbkit_sp_${savepointCounter}`;
24
+ }
@@ -0,0 +1,66 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { createSerialQueue, savepointName } from "./queue.ts";
4
+ import type { Executor, OpenConfig } from "./types.ts";
5
+
6
+ /** SQLite 连接调优:打开即设 PRAGMA,返回这条连接上的执行面。 */
7
+ export function openSqlite(config: Extract<OpenConfig, { dialect: "sqlite" }>): Executor {
8
+ const client = new DatabaseSync(config.path);
9
+ client.exec("PRAGMA foreign_keys = ON");
10
+ client.exec(`PRAGMA busy_timeout = ${config.busyTimeout ?? 5000}`);
11
+ if (config.cacheSizeKb !== undefined) client.exec(`PRAGMA cache_size = -${config.cacheSizeKb}`);
12
+ if (config.mmapSize !== undefined) client.exec(`PRAGMA mmap_size = ${config.mmapSize}`);
13
+ if (config.journalMode !== undefined && config.applyJournalMode !== false) {
14
+ client.exec(`PRAGMA journal_mode = ${config.journalMode}`);
15
+ }
16
+ const enqueue = createSerialQueue();
17
+ // 当前异步上下文所在的事务(嵌套时共享同一条连接)。
18
+ const current = new AsyncLocalStorage<true>();
19
+ const executor: Executor = {
20
+ async *iterate(text, params) {
21
+ // node:sqlite 的 `iterate` 是同步生成器:逐行读,不把结果集收成数组。
22
+ for (const row of client.prepare(text).iterate(...(params as never[]))) yield row;
23
+ },
24
+ run(text, params) {
25
+ const write = async (): Promise<void> => {
26
+ client.prepare(text).run(...(params as never[]));
27
+ };
28
+ // 事务内的写就是这次事务的一部分;事务外的写先排队,免得落到别人的未提交事务里。
29
+ return current.getStore() === true ? write() : enqueue(write);
30
+ },
31
+ transaction(fn) {
32
+ if (current.getStore() === true) {
33
+ // 嵌套:同一条连接上的保存点,内层失败只回滚内层。
34
+ return (async () => {
35
+ const name = savepointName();
36
+ client.exec(`SAVEPOINT ${name}`);
37
+ try {
38
+ const result = await current.run(true, () => fn(executor));
39
+ client.exec(`RELEASE SAVEPOINT ${name}`);
40
+ return result;
41
+ } catch (error) {
42
+ client.exec(`ROLLBACK TO SAVEPOINT ${name}`);
43
+ client.exec(`RELEASE SAVEPOINT ${name}`);
44
+ throw error;
45
+ }
46
+ })();
47
+ }
48
+ return enqueue(async () => {
49
+ client.exec("BEGIN IMMEDIATE");
50
+ try {
51
+ const result = await current.run(true, () => fn(executor));
52
+ client.exec("COMMIT");
53
+ return result;
54
+ } catch (error) {
55
+ client.exec("ROLLBACK");
56
+ throw error;
57
+ }
58
+ });
59
+ },
60
+ async close() {
61
+ await enqueue(async () => undefined);
62
+ client.close();
63
+ },
64
+ };
65
+ return executor;
66
+ }
@@ -0,0 +1,87 @@
1
+ import type { CatalogIndex } from "../catalog.ts";
2
+ import type { Catalog, TableDef } from "../defs.ts";
3
+ import type { Dialect, DialectName } from "../dialect.ts";
4
+ import type { Fragment } from "../fragment.ts";
5
+ import type { TableHandle } from "../table/handle.ts";
6
+
7
+ /**
8
+ * 注入给业务的库句柄:只回答「哪张表」与「怎么读/写/开事务」;不暴露连接与 SQL。
9
+ * `dialect` 只给迁移 / 日志这类基础层用,业务查询不需要它。
10
+ * 连接生命周期由创建它的一方(装配层)管理,业务拿到的是这个接口。
11
+ */
12
+ export interface Database<C extends Catalog = Catalog> {
13
+ /** 迁移 / 日志用;业务不需要读它。 */
14
+ readonly dialect: Dialect;
15
+ /** 装配层注册的 catalog(按表名解析;没注册时只能传定义)。 */
16
+ readonly catalog: CatalogIndex;
17
+ /** 按表名取(catalog 里有定义时保留强类型)。 */
18
+ table<K extends C[number]["name"] & string>(
19
+ name: K,
20
+ ): TableHandle<Extract<C[number], { name: K }>>;
21
+ /** 直接传定义(catalog 未注册时)。 */
22
+ table<T extends TableDef>(definition: T): TableHandle<T>;
23
+ /** 主读路径:按行流式迭代(与 Golang 的 `iter.Seq2` 对齐,不一次性物化大结果)。 */
24
+ iterate<T>(query: Fragment): AsyncIterable<T>;
25
+ /** 便捷读:把迭代器收成数组(报表 / 迁移读取用)。 */
26
+ read<T>(query: Fragment): Promise<T[]>;
27
+ /** 写入并拿回行(`RETURNING`;没有 RETURNING 的语句返回空数组)。 */
28
+ write<T>(query: Fragment): Promise<T[]>;
29
+ run(query: Fragment): Promise<void>;
30
+ transaction<T>(fn: (database: Database<C>) => Promise<T>): Promise<T>;
31
+ /** 现有的表与视图名(迁移探测旧表、拒绝别人的库都要它)。 */
32
+ objectNames(): Promise<string[]>;
33
+ /** 回收空间与统计(SQLite `VACUUM` / PostgreSQL `VACUUM ANALYZE`);不得在事务内调用。 */
34
+ vacuum(): Promise<void>;
35
+ /** 读 SQLite 的 `PRAGMA`(其它方言直接报不支持)。 */
36
+ sqlitePragma(name: SqlitePragmaName): Promise<string | number | undefined>;
37
+ /** 写 SQLite 的 `PRAGMA`(值只允许安全字面量,见 `assertPragmaValue`)。 */
38
+ setSqlitePragma(name: SqlitePragmaName, value: string | number): Promise<void>;
39
+ close(): Promise<void>;
40
+ }
41
+
42
+ /** 业务侧用得到的 SQLite PRAGMA(库身份与日志模式;其余不开口子)。 */
43
+ export type SqlitePragmaName = "user_version" | "application_id" | "journal_mode";
44
+
45
+ export type OpenConfig =
46
+ | {
47
+ dialect: "sqlite";
48
+ path: string;
49
+ journalMode?: "wal" | "delete" | "truncate" | "persist";
50
+ busyTimeout?: number;
51
+ /** SQLite 页缓存(KB,负数表示 KB 数;不传用 SQLite 默认)。 */
52
+ cacheSizeKb?: number;
53
+ /** mmap 窗口(字节;0 关闭)。 */
54
+ mmapSize?: number;
55
+ /** 是否在打开时立即设置 `journal_mode`(默认 true;需要先校验库再切的调用方传 false)。 */
56
+ applyJournalMode?: boolean;
57
+ }
58
+ | {
59
+ dialect: "postgres";
60
+ connectionString: string;
61
+ schema?: string;
62
+ /** 事务隔离级别(session-rdb 的写路径要 `serializable`)。 */
63
+ isolationLevel?: "read committed" | "repeatable read" | "serializable";
64
+ };
65
+
66
+ /** 一条已执行语句的观测(日志 / 慢查询 / 耗时统计)。 */
67
+ export interface QueryEvent {
68
+ readonly dialect: DialectName;
69
+ readonly sql: string;
70
+ readonly params: readonly unknown[];
71
+ readonly durationMs: number;
72
+ readonly error?: unknown;
73
+ }
74
+
75
+ /** 打开时可带 catalog 与查询观测钩子。 */
76
+ export type OpenConfigWithCatalog<C extends Catalog = Catalog> = OpenConfig & {
77
+ catalog?: C;
78
+ onQuery?: (event: QueryEvent) => void;
79
+ };
80
+
81
+ /** 一条连接(或一个已开启的事务连接)的执行面:两种方言只实现这四个原语。 */
82
+ export interface Executor {
83
+ iterate(text: string, params: readonly unknown[]): AsyncIterable<unknown>;
84
+ run(text: string, params: readonly unknown[]): Promise<void>;
85
+ transaction<T>(fn: (executor: Executor) => Promise<T>): Promise<T>;
86
+ close(): Promise<void>;
87
+ }