@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.
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/dist/index.d.mts +518 -0
- package/dist/index.mjs +1422 -0
- package/dist/testing.d.mts +28 -0
- package/dist/testing.mjs +42 -0
- package/package.json +49 -3
- package/src/__tests__/database-introspect.spec.ts +71 -0
- package/src/__tests__/expr-functions.spec.ts +64 -0
- package/src/__tests__/migration.spec.ts +81 -0
- package/src/__tests__/pg.spec.ts +123 -0
- package/src/__tests__/query-join.spec.ts +114 -0
- package/src/__tests__/query-p1.spec.ts +108 -0
- package/src/__tests__/query-write.spec.ts +293 -0
- package/src/__tests__/query.spec.ts +88 -0
- package/src/__tests__/schema.spec.ts +144 -0
- package/src/__tests__/transaction.spec.ts +83 -0
- package/src/catalog.ts +24 -0
- package/src/compound.ts +64 -0
- package/src/database/capabilities.ts +70 -0
- package/src/database/create.ts +119 -0
- package/src/database/index.ts +9 -0
- package/src/database/postgres.ts +133 -0
- package/src/database/queue.ts +24 -0
- package/src/database/sqlite.ts +66 -0
- package/src/database/types.ts +87 -0
- package/src/database/wrap.ts +48 -0
- package/src/defs.ts +82 -0
- package/src/dialect.ts +219 -0
- package/src/error.ts +72 -0
- package/src/expr.ts +346 -0
- package/src/fragment.ts +96 -0
- package/src/index.ts +114 -0
- package/src/migrate.ts +133 -0
- package/src/migration.ts +82 -0
- package/src/table/handle.ts +142 -0
- package/src/table/index.ts +14 -0
- package/src/table/render.ts +140 -0
- package/src/table/select.ts +229 -0
- package/src/table/types.ts +53 -0
- package/src/testing.ts +56 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
column,
|
|
4
|
+
defineTable,
|
|
5
|
+
ensureSchema,
|
|
6
|
+
eq,
|
|
7
|
+
forUpdate,
|
|
8
|
+
fragment,
|
|
9
|
+
gt,
|
|
10
|
+
openDatabase,
|
|
11
|
+
render,
|
|
12
|
+
union,
|
|
13
|
+
value,
|
|
14
|
+
withCtes,
|
|
15
|
+
type QueryEvent,
|
|
16
|
+
} from "../index.ts";
|
|
17
|
+
|
|
18
|
+
const items = defineTable({
|
|
19
|
+
name: "t_items",
|
|
20
|
+
columns: {
|
|
21
|
+
f_id: { type: "serial", primaryKey: true },
|
|
22
|
+
f_key: { type: "text", notNull: true, unique: true },
|
|
23
|
+
f_count: { type: "integer", notNull: true, default: 0 },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("P1:join 变体 / CTE / UNION / 行锁 / 查询观测", () => {
|
|
28
|
+
it("join 变体与别名、CTE、UNION 的 SQL 形状(两方言)", () => {
|
|
29
|
+
const table = defineTable(items);
|
|
30
|
+
const query = { table } as never;
|
|
31
|
+
void query;
|
|
32
|
+
const left = fragment('SELECT "f_id" FROM "t_items" WHERE "f_count" > ?', [1]);
|
|
33
|
+
const right = fragment('SELECT "f_id" FROM "t_items" WHERE "f_count" > ?', [2]);
|
|
34
|
+
expect(render(union(left, right), "sqlite")).toEqual({
|
|
35
|
+
text: 'SELECT "f_id" FROM "t_items" WHERE "f_count" > ? UNION SELECT "f_id" FROM "t_items" WHERE "f_count" > ?',
|
|
36
|
+
params: [1, 2],
|
|
37
|
+
});
|
|
38
|
+
expect(render(union(left, right, { all: true }), "postgres").text).toContain("UNION ALL");
|
|
39
|
+
|
|
40
|
+
const cte = withCtes(
|
|
41
|
+
[{ name: "recent", query: left }],
|
|
42
|
+
fragment('SELECT "f_id" FROM "recent"'),
|
|
43
|
+
);
|
|
44
|
+
expect(render(cte, "postgres").text).toBe(
|
|
45
|
+
'WITH "recent" AS ( SELECT "f_id" FROM "t_items" WHERE "f_count" > $1 ) SELECT "f_id" FROM "recent"',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// 行锁只有 PostgreSQL 认:SQLite 渲染时整段丢掉(参数也不留)。
|
|
49
|
+
expect(render(forUpdate({ skipLocked: true }), "sqlite")).toEqual({ text: "", params: [] });
|
|
50
|
+
expect(render(forUpdate({ skipLocked: true }), "postgres").text).toBe("FOR UPDATE SKIP LOCKED");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("实跑:join 别名、CTE、UNION、查询观测", async () => {
|
|
54
|
+
const events: QueryEvent[] = [];
|
|
55
|
+
const database = await openDatabase({
|
|
56
|
+
dialect: "sqlite",
|
|
57
|
+
path: ":memory:",
|
|
58
|
+
onQuery: (event) => events.push(event),
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await ensureSchema(database, [items]);
|
|
62
|
+
const table = database.table(items);
|
|
63
|
+
await table.insertMany([
|
|
64
|
+
{ f_key: "a", f_count: 1 },
|
|
65
|
+
{ f_key: "b", f_count: 2 },
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// join 别名 + 左连接
|
|
69
|
+
const rows: { key: string; other: string | null }[] = [];
|
|
70
|
+
for await (const row of table
|
|
71
|
+
.select({ key: table.column("f_key"), other: column("f_key", "o") })
|
|
72
|
+
.join(items, eq(column("f_id", "o"), column("f_id", "t_items")), "left", "o")
|
|
73
|
+
.orderBy("f_key")) {
|
|
74
|
+
rows.push(row as never);
|
|
75
|
+
}
|
|
76
|
+
expect(rows.map((row) => row.key)).toEqual(["a", "b"]);
|
|
77
|
+
|
|
78
|
+
// CTE + 主查询
|
|
79
|
+
const cte = withCtes(
|
|
80
|
+
[{ name: "recent", query: table.select({ id: table.column("f_id") }).toFragment() }],
|
|
81
|
+
fragment('SELECT "id" FROM "recent" ORDER BY "id"'),
|
|
82
|
+
);
|
|
83
|
+
const ids = await database.read<{ id: number }>(cte);
|
|
84
|
+
expect(ids.map((row) => row.id)).toEqual([1, 2]);
|
|
85
|
+
|
|
86
|
+
// UNION
|
|
87
|
+
const merged = await database.read<{ id: number }>(
|
|
88
|
+
union(
|
|
89
|
+
table
|
|
90
|
+
.select({ id: table.column("f_id") })
|
|
91
|
+
.where(gt(column("f_count"), value(1)))
|
|
92
|
+
.toFragment(),
|
|
93
|
+
table
|
|
94
|
+
.select({ id: table.column("f_id") })
|
|
95
|
+
.where(eq(column("f_key"), value("a")))
|
|
96
|
+
.toFragment(),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
expect(new Set(merged.map((row) => row.id))).toEqual(new Set([1, 2]));
|
|
100
|
+
|
|
101
|
+
expect(events.length).toBeGreaterThan(0);
|
|
102
|
+
expect(events[0]).toMatchObject({ dialect: "sqlite" });
|
|
103
|
+
expect(typeof events[0]?.durationMs).toBe("number");
|
|
104
|
+
} finally {
|
|
105
|
+
await database.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
column,
|
|
4
|
+
count,
|
|
5
|
+
defineTable,
|
|
6
|
+
ensureSchema,
|
|
7
|
+
eq,
|
|
8
|
+
exists,
|
|
9
|
+
gt,
|
|
10
|
+
expr,
|
|
11
|
+
inSubquery,
|
|
12
|
+
openDatabase,
|
|
13
|
+
value,
|
|
14
|
+
} from "../index.ts";
|
|
15
|
+
|
|
16
|
+
const items = defineTable({
|
|
17
|
+
name: "t_items",
|
|
18
|
+
columns: {
|
|
19
|
+
f_id: { type: "serial", primaryKey: true },
|
|
20
|
+
f_key: { type: "text", notNull: true, unique: true },
|
|
21
|
+
f_count: { type: "integer", notNull: true, default: 0 },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const audit = defineTable({
|
|
26
|
+
name: "t_audit",
|
|
27
|
+
columns: {
|
|
28
|
+
f_item: { type: "integer", notNull: true },
|
|
29
|
+
f_note: { type: "text" },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("写入通道(RETURNING / 冲突表达式 / 子查询 / 错误分类)", () => {
|
|
34
|
+
it("insert / update 拿回行,冲突时按表达式自增", async () => {
|
|
35
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
36
|
+
try {
|
|
37
|
+
await ensureSchema(database, [items, audit]);
|
|
38
|
+
const table = database.table(items);
|
|
39
|
+
|
|
40
|
+
const [created] = await table.insert({ f_key: "a" }, ["f_id", "f_count"]);
|
|
41
|
+
expect(created).toMatchObject({ f_id: 1, f_count: 0 });
|
|
42
|
+
|
|
43
|
+
const [updated] = await table.update({ f_count: 10 }, eq(column("f_key"), value("a")), [
|
|
44
|
+
"f_count",
|
|
45
|
+
]);
|
|
46
|
+
expect(updated).toMatchObject({ f_count: 10 });
|
|
47
|
+
|
|
48
|
+
// 冲突时用表达式更新:计数自增,而不是覆盖。
|
|
49
|
+
await table.upsertExpr({ f_key: "a", f_count: 1 }, ["f_key"], {
|
|
50
|
+
f_count: expr('"f_count" + 1'),
|
|
51
|
+
});
|
|
52
|
+
let afterUpsert: { count: number } | undefined;
|
|
53
|
+
for await (const row of table.select({ count: table.column("f_count") })) {
|
|
54
|
+
afterUpsert = row;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
expect(afterUpsert).toEqual({ count: 11 });
|
|
58
|
+
} finally {
|
|
59
|
+
await database.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("子查询:IN (SELECT) 与 EXISTS", async () => {
|
|
64
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
65
|
+
try {
|
|
66
|
+
await ensureSchema(database, [items, audit]);
|
|
67
|
+
const table = database.table(items);
|
|
68
|
+
const auditTable = database.table(audit);
|
|
69
|
+
await table.insert({ f_key: "a" }, ["f_id"]);
|
|
70
|
+
await table.insert({ f_key: "b" }, ["f_id"]);
|
|
71
|
+
await auditTable.insertMany([{ f_item: 2, f_note: "seen" }]);
|
|
72
|
+
|
|
73
|
+
const withAudit: string[] = [];
|
|
74
|
+
for await (const row of table
|
|
75
|
+
.select({ key: table.column("f_key") })
|
|
76
|
+
.where(
|
|
77
|
+
inSubquery(
|
|
78
|
+
column("f_id"),
|
|
79
|
+
auditTable.select({ item: auditTable.column("f_item") }).toFragment(),
|
|
80
|
+
),
|
|
81
|
+
)) {
|
|
82
|
+
withAudit.push(row.key);
|
|
83
|
+
}
|
|
84
|
+
expect(withAudit).toEqual(["b"]);
|
|
85
|
+
|
|
86
|
+
const existsRows: number[] = [];
|
|
87
|
+
for await (const row of table.select({ id: table.column("f_id") }).where(
|
|
88
|
+
exists(
|
|
89
|
+
auditTable
|
|
90
|
+
.select({ one: auditTable.column("f_item") })
|
|
91
|
+
.where(eq(column("f_item", "t_audit"), column("f_id", "t_items")))
|
|
92
|
+
.toFragment(),
|
|
93
|
+
),
|
|
94
|
+
)) {
|
|
95
|
+
existsRows.push(row.id);
|
|
96
|
+
}
|
|
97
|
+
expect(existsRows).toEqual([2]);
|
|
98
|
+
} finally {
|
|
99
|
+
await database.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("错误分类:唯一冲突与非空约束", async () => {
|
|
104
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
105
|
+
try {
|
|
106
|
+
await ensureSchema(database, [items]);
|
|
107
|
+
const table = database.table(items);
|
|
108
|
+
await table.insert({ f_key: "a" });
|
|
109
|
+
await expect(table.insert({ f_key: "a" })).rejects.toMatchObject({ kind: "unique" });
|
|
110
|
+
await expect(table.insert({ f_key: null as never })).rejects.toMatchObject({
|
|
111
|
+
kind: "not-null",
|
|
112
|
+
});
|
|
113
|
+
} finally {
|
|
114
|
+
await database.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("批量写入", () => {
|
|
120
|
+
it("insertMany + ON CONFLICT 覆盖:一行不多、值被更新", async () => {
|
|
121
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
122
|
+
try {
|
|
123
|
+
await ensureSchema(database, [items]);
|
|
124
|
+
const table = database.table(items);
|
|
125
|
+
await table.insertMany(
|
|
126
|
+
[
|
|
127
|
+
{ f_key: "a", f_count: 1 },
|
|
128
|
+
{ f_key: "b", f_count: 2 },
|
|
129
|
+
],
|
|
130
|
+
{ conflict: ["f_key"], update: ["f_count"] },
|
|
131
|
+
);
|
|
132
|
+
await table.insertMany([{ f_key: "a", f_count: 9 }], {
|
|
133
|
+
conflict: ["f_key"],
|
|
134
|
+
update: ["f_count"],
|
|
135
|
+
});
|
|
136
|
+
const rows: { key: string; count: number }[] = [];
|
|
137
|
+
for await (const row of table
|
|
138
|
+
.select({ key: table.column("f_key"), count: table.column("f_count") })
|
|
139
|
+
.orderBy("f_key")) {
|
|
140
|
+
rows.push(row);
|
|
141
|
+
}
|
|
142
|
+
expect(rows).toEqual([
|
|
143
|
+
{ key: "a", count: 9 },
|
|
144
|
+
{ key: "b", count: 2 },
|
|
145
|
+
]);
|
|
146
|
+
} finally {
|
|
147
|
+
await database.close();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("事务端口(宿主管事务,执行面按调用传入)", () => {
|
|
153
|
+
it("scoped Database 用同一事务执行,回滚后什么都不留", async () => {
|
|
154
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
155
|
+
const { wrapDatabase } = await import("../index.ts");
|
|
156
|
+
const client = new DatabaseSync(":memory:");
|
|
157
|
+
const scoped = (): ReturnType<typeof wrapDatabase> =>
|
|
158
|
+
wrapDatabase("sqlite", {
|
|
159
|
+
iterate: async function* (text: string, params: readonly unknown[]) {
|
|
160
|
+
for (const row of client.prepare(text).iterate(...(params as never[]))) yield row;
|
|
161
|
+
},
|
|
162
|
+
run: async (text: string, params: readonly unknown[]) => {
|
|
163
|
+
client.prepare(text).run(...(params as never[]));
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const database = wrapDatabase("sqlite", {
|
|
167
|
+
iterate: async function* (text: string, params: readonly unknown[]) {
|
|
168
|
+
for (const row of client.prepare(text).iterate(...(params as never[]))) yield row;
|
|
169
|
+
},
|
|
170
|
+
run: async (text: string, params: readonly unknown[]) => {
|
|
171
|
+
client.prepare(text).run(...(params as never[]));
|
|
172
|
+
},
|
|
173
|
+
transaction: async (fn) => {
|
|
174
|
+
client.exec("BEGIN IMMEDIATE");
|
|
175
|
+
try {
|
|
176
|
+
const result = await fn(scoped());
|
|
177
|
+
client.exec("COMMIT");
|
|
178
|
+
return result;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
client.exec("ROLLBACK");
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
await ensureSchema(database, [items]);
|
|
187
|
+
const table = database.table(items);
|
|
188
|
+
await database.transaction(async (tx) => {
|
|
189
|
+
await tx.table(items).insert({ f_key: "a" });
|
|
190
|
+
});
|
|
191
|
+
let total = 0;
|
|
192
|
+
for await (const row of table.select({ n: count() })) total = Number(row.n);
|
|
193
|
+
expect(total).toBe(1);
|
|
194
|
+
await expect(
|
|
195
|
+
database.transaction(async (tx) => {
|
|
196
|
+
await tx.table(items).insert({ f_key: "b" });
|
|
197
|
+
throw new Error("boom");
|
|
198
|
+
}),
|
|
199
|
+
).rejects.toThrow("boom");
|
|
200
|
+
let after = 0;
|
|
201
|
+
for await (const row of table.select({ n: count() })) after = Number(row.n);
|
|
202
|
+
expect(after).toBe(1);
|
|
203
|
+
} finally {
|
|
204
|
+
client.close();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("更新表达式", () => {
|
|
210
|
+
it("update 里可以写表达式(列自增 / 取大),普通值仍然绑定", async () => {
|
|
211
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
212
|
+
try {
|
|
213
|
+
await ensureSchema(database, [items]);
|
|
214
|
+
const table = database.table(items);
|
|
215
|
+
await table.insert({ f_key: "a", f_count: 1 }, ["f_id"]);
|
|
216
|
+
await table.update({ f_count: expr('"f_count" + 10') }, eq(table.f_key, "a"));
|
|
217
|
+
let seen = -1;
|
|
218
|
+
for await (const row of table.select({ count: table.f_count })) seen = row.count;
|
|
219
|
+
expect(seen).toBe(11);
|
|
220
|
+
} finally {
|
|
221
|
+
await database.close();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("删除", () => {
|
|
227
|
+
it("delete 可以拿回被删的行(RETURNING)", async () => {
|
|
228
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
229
|
+
try {
|
|
230
|
+
await ensureSchema(database, [items]);
|
|
231
|
+
const table = database.table(items);
|
|
232
|
+
await table.insertMany([{ f_key: "a" }, { f_key: "b" }]);
|
|
233
|
+
const removed = await table.delete(eq(table.f_key, "a"), ["f_key"]);
|
|
234
|
+
expect(removed).toEqual([{ f_key: "a" }]);
|
|
235
|
+
expect(await table.delete(eq(table.f_key, "missing"), ["f_key"])).toEqual([]);
|
|
236
|
+
} finally {
|
|
237
|
+
await database.close();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("INSERT … SELECT", () => {
|
|
243
|
+
it("按投影顺序把查询结果插进来(可带冲突子句)", async () => {
|
|
244
|
+
const archive = defineTable({
|
|
245
|
+
name: "t_archive",
|
|
246
|
+
columns: {
|
|
247
|
+
f_id: { type: "serial", primaryKey: true },
|
|
248
|
+
f_key: { type: "text", notNull: true, unique: true },
|
|
249
|
+
f_count: { type: "integer", notNull: true, default: 0 },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
253
|
+
try {
|
|
254
|
+
await ensureSchema(database, [items, archive]);
|
|
255
|
+
const table = database.table(items);
|
|
256
|
+
const target = database.table(archive);
|
|
257
|
+
await table.insertMany([
|
|
258
|
+
{ f_key: "a", f_count: 1 },
|
|
259
|
+
{ f_key: "b", f_count: 2 },
|
|
260
|
+
]);
|
|
261
|
+
await target.insertFromSelect(
|
|
262
|
+
["f_key", "f_count"],
|
|
263
|
+
table
|
|
264
|
+
.select({ key: table.f_key, count: table.f_count })
|
|
265
|
+
.where(gt(table.f_count, 1))
|
|
266
|
+
.toFragment(),
|
|
267
|
+
);
|
|
268
|
+
const copied: { key: string; count: number }[] = [];
|
|
269
|
+
for await (const row of target.select({ key: target.f_key, count: target.f_count })) {
|
|
270
|
+
copied.push(row);
|
|
271
|
+
}
|
|
272
|
+
expect(copied).toEqual([{ key: "b", count: 2 }]);
|
|
273
|
+
} finally {
|
|
274
|
+
await database.close();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("defineTable 即操作钩子", () => {
|
|
280
|
+
it("表定义直接拿句柄(与 database.table(def) 等价)", async () => {
|
|
281
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
282
|
+
try {
|
|
283
|
+
await ensureSchema(database, [items]);
|
|
284
|
+
const table = items.table(database);
|
|
285
|
+
await table.insert({ f_key: "hook" });
|
|
286
|
+
const seen: string[] = [];
|
|
287
|
+
for await (const row of table.select({ key: table.f_key })) seen.push(row.key);
|
|
288
|
+
expect(seen).toEqual(["hook"]);
|
|
289
|
+
} finally {
|
|
290
|
+
await database.close();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
and,
|
|
4
|
+
column,
|
|
5
|
+
count,
|
|
6
|
+
defineTable,
|
|
7
|
+
eq,
|
|
8
|
+
fragment,
|
|
9
|
+
inList,
|
|
10
|
+
isNull,
|
|
11
|
+
openDatabase,
|
|
12
|
+
render,
|
|
13
|
+
value,
|
|
14
|
+
} from "../index.ts";
|
|
15
|
+
|
|
16
|
+
const users = defineTable({
|
|
17
|
+
name: "t_users",
|
|
18
|
+
columns: {
|
|
19
|
+
f_id: { type: "serial", primaryKey: true },
|
|
20
|
+
f_name: { type: "text", notNull: true },
|
|
21
|
+
f_note: { type: "text" },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("投影查询(业务不写 SQL)", () => {
|
|
26
|
+
it("select 的投影决定结果类型,读是迭代器", async () => {
|
|
27
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
28
|
+
try {
|
|
29
|
+
await database.run(
|
|
30
|
+
fragment(
|
|
31
|
+
'CREATE TABLE "t_users" ("f_id" INTEGER PRIMARY KEY AUTOINCREMENT, "f_name" TEXT NOT NULL, "f_note" TEXT)',
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
const table = database.table(users);
|
|
35
|
+
await table.insert({ f_name: "ada" });
|
|
36
|
+
await table.insert({ f_name: "bob", f_note: "ops" });
|
|
37
|
+
|
|
38
|
+
const names: string[] = [];
|
|
39
|
+
for await (const row of table
|
|
40
|
+
.select({ name: table.column("f_name") })
|
|
41
|
+
.where(eq(column("f_name"), value("ada")))) {
|
|
42
|
+
names.push(row.name);
|
|
43
|
+
}
|
|
44
|
+
expect(names).toEqual(["ada"]);
|
|
45
|
+
|
|
46
|
+
await table.update({ f_note: "ops-updated" }, eq(column("f_name"), value("bob")));
|
|
47
|
+
const bob = await first(
|
|
48
|
+
table.select({ note: table.column("f_note") }).where(eq(column("f_name"), value("bob"))),
|
|
49
|
+
);
|
|
50
|
+
expect(bob).toEqual({ note: "ops-updated" });
|
|
51
|
+
|
|
52
|
+
const [total] = await collect(table.select({ n: count() }));
|
|
53
|
+
expect(total?.n).toBe(2);
|
|
54
|
+
|
|
55
|
+
await table.delete(and(eq(column("f_name"), value("ada")), isNull(column("f_note"))));
|
|
56
|
+
const [after] = await collect(table.select({ n: count() }));
|
|
57
|
+
expect(after?.n).toBe(1);
|
|
58
|
+
} finally {
|
|
59
|
+
await database.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("片段是惰性迭代器:可重复消费,占位符只由参数节点生成", () => {
|
|
64
|
+
const condition = eq(column("f_name"), value("ada"));
|
|
65
|
+
expect(render(condition, "sqlite")).toEqual({ text: '"f_name" = ?', params: ["ada"] });
|
|
66
|
+
expect(render(condition, "postgres")).toEqual({ text: '"f_name" = $1', params: ["ada"] });
|
|
67
|
+
// 可重复消费:每次调用重新生成,不是一次性迭代器。
|
|
68
|
+
expect(render(condition, "sqlite")).toEqual({ text: '"f_name" = ?', params: ["ada"] });
|
|
69
|
+
|
|
70
|
+
// 多条件组合一律加括号(AND/OR 混用时优先级不靠读 SQL 的人来判断)。
|
|
71
|
+
const combined = and(condition, inList(column("f_id"), [1, 2]));
|
|
72
|
+
expect(render(combined, "postgres")).toEqual({
|
|
73
|
+
text: '("f_name" = $1 AND "f_id" IN ($2, $3))',
|
|
74
|
+
params: ["ada", 1, 2],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function collect<T>(rows: AsyncIterable<T>): Promise<T[]> {
|
|
80
|
+
const result: T[] = [];
|
|
81
|
+
for await (const row of rows) result.push(row);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function first<T>(rows: AsyncIterable<T>): Promise<T | undefined> {
|
|
86
|
+
for await (const row of rows) return row;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createDialect,
|
|
4
|
+
createTableSql,
|
|
5
|
+
defineCatalog,
|
|
6
|
+
defineTable,
|
|
7
|
+
ensureSchema,
|
|
8
|
+
fragment,
|
|
9
|
+
openDatabase,
|
|
10
|
+
} from "../index.ts";
|
|
11
|
+
|
|
12
|
+
const catalog = defineCatalog([
|
|
13
|
+
defineTable({
|
|
14
|
+
name: "t_items",
|
|
15
|
+
columns: {
|
|
16
|
+
f_id: { type: "serial", primaryKey: true },
|
|
17
|
+
f_owner: { type: "text", notNull: true },
|
|
18
|
+
f_count: { type: "integer", notNull: true, default: 0 },
|
|
19
|
+
f_active: { type: "boolean", notNull: true, default: 1 },
|
|
20
|
+
},
|
|
21
|
+
indexes: { idx_items_owner: { columns: ["f_owner"] } },
|
|
22
|
+
}),
|
|
23
|
+
defineTable({
|
|
24
|
+
name: "t_links",
|
|
25
|
+
columns: {
|
|
26
|
+
f_owner: {
|
|
27
|
+
type: "text",
|
|
28
|
+
notNull: true,
|
|
29
|
+
references: { table: "t_items", column: "f_owner", onDelete: "cascade" },
|
|
30
|
+
},
|
|
31
|
+
f_key: { type: "text", notNull: true },
|
|
32
|
+
},
|
|
33
|
+
primaryKey: ["f_owner", "f_key"],
|
|
34
|
+
uniques: { uq_links_key: { columns: ["f_owner", "f_key"] } },
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
describe("DDL 由表定义生成", () => {
|
|
39
|
+
it("SQLite 与 PostgreSQL 各自的类型 / 主键 / 引用", () => {
|
|
40
|
+
const sqlite = createDialect("sqlite");
|
|
41
|
+
const item = catalog[0]!;
|
|
42
|
+
const sqliteSql = createTableSql(sqlite, item);
|
|
43
|
+
expect(sqliteSql).toContain('"f_id" INTEGER PRIMARY KEY AUTOINCREMENT');
|
|
44
|
+
expect(sqliteSql).toContain('"f_active" INTEGER NOT NULL DEFAULT 1');
|
|
45
|
+
expect(sqliteSql).toContain('CREATE TABLE IF NOT EXISTS "t_items"');
|
|
46
|
+
|
|
47
|
+
const postgres = createDialect("postgres");
|
|
48
|
+
const postgresSql = createTableSql(postgres, item);
|
|
49
|
+
expect(postgresSql).toContain('"f_id" SERIAL PRIMARY KEY');
|
|
50
|
+
expect(postgresSql).toContain('"f_active" BOOLEAN NOT NULL DEFAULT TRUE');
|
|
51
|
+
expect(postgresSql).toContain('CREATE TABLE IF NOT EXISTS "public"."t_items"');
|
|
52
|
+
|
|
53
|
+
const link = catalog[1]!;
|
|
54
|
+
expect(createTableSql(sqlite, link)).toContain(
|
|
55
|
+
'REFERENCES "t_items"("f_owner") ON DELETE CASCADE',
|
|
56
|
+
);
|
|
57
|
+
expect(createTableSql(sqlite, link)).toContain('PRIMARY KEY ("f_owner", "f_key")');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("ensureSchema 只做增", () => {
|
|
62
|
+
it("建表、补列、补索引,并且重复执行是幂等的", async () => {
|
|
63
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
64
|
+
try {
|
|
65
|
+
const first = await ensureSchema(database, catalog);
|
|
66
|
+
expect(first.some((sql) => sql.startsWith('CREATE TABLE IF NOT EXISTS "t_items"'))).toBe(
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 第二次:结构已对齐,不再执行任何语句。
|
|
71
|
+
expect(await ensureSchema(database, catalog)).toEqual([]);
|
|
72
|
+
|
|
73
|
+
// 定义里多一列:只在旧表上补这一列。
|
|
74
|
+
const extended = defineCatalog([
|
|
75
|
+
defineTable({
|
|
76
|
+
...catalog[0]!,
|
|
77
|
+
columns: { ...catalog[0]!.columns, f_note: { type: "text" } },
|
|
78
|
+
}),
|
|
79
|
+
catalog[1]!,
|
|
80
|
+
]);
|
|
81
|
+
const applied = await ensureSchema(database, extended);
|
|
82
|
+
expect(applied).toEqual(['ALTER TABLE "t_items" ADD COLUMN "f_note" TEXT']);
|
|
83
|
+
|
|
84
|
+
const columns = await database.read<{ name: string }>(
|
|
85
|
+
fragment('PRAGMA table_info("t_items")'),
|
|
86
|
+
);
|
|
87
|
+
expect(columns.map((row) => row.name)).toContain("f_note");
|
|
88
|
+
} finally {
|
|
89
|
+
await database.close();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("已有数据的表加 NOT NULL 列(无默认值)时 fail loud", async () => {
|
|
94
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
95
|
+
try {
|
|
96
|
+
await ensureSchema(database, catalog);
|
|
97
|
+
const strict = defineCatalog([
|
|
98
|
+
defineTable({
|
|
99
|
+
...catalog[0]!,
|
|
100
|
+
columns: { ...catalog[0]!.columns, f_note: { type: "text", notNull: true } },
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
await expect(ensureSchema(database, strict)).rejects.toThrow(/without a default/u);
|
|
104
|
+
} finally {
|
|
105
|
+
await database.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("catalog 注入后按表名取表;允许破坏性变更时才删多余列", async () => {
|
|
110
|
+
const database = await openDatabase({ dialect: "sqlite", path: ":memory:" });
|
|
111
|
+
try {
|
|
112
|
+
await ensureSchema(database, catalog);
|
|
113
|
+
// 定义里没有 f_legacy:默认保留(升级不丢数据)。
|
|
114
|
+
await database.run(fragment('ALTER TABLE "t_items" ADD COLUMN "f_legacy" TEXT'));
|
|
115
|
+
expect(await ensureSchema(database, catalog)).toEqual([]);
|
|
116
|
+
const dropped = await ensureSchema(database, catalog, { allowDestructive: true });
|
|
117
|
+
expect(dropped).toEqual(['ALTER TABLE "t_items" DROP COLUMN "f_legacy"']);
|
|
118
|
+
} finally {
|
|
119
|
+
await database.close();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("SQLite 连接调优选项", () => {
|
|
125
|
+
it("cacheSizeKb / mmapSize 落到 PRAGMA(连接级,从同一连接读回)", async () => {
|
|
126
|
+
const { openDatabase } = await import("../index.ts");
|
|
127
|
+
const { fragment } = await import("../index.ts");
|
|
128
|
+
const path = `/tmp/rdbkit-pragma-${Date.now()}.sqlite`;
|
|
129
|
+
const database = await openDatabase({
|
|
130
|
+
dialect: "sqlite",
|
|
131
|
+
path,
|
|
132
|
+
cacheSizeKb: 65536,
|
|
133
|
+
mmapSize: 268435456,
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
const [cache] = await database.read<{ cache_size: number }>(fragment("PRAGMA cache_size"));
|
|
137
|
+
const [mmap] = await database.read<{ mmap_size: number }>(fragment("PRAGMA mmap_size"));
|
|
138
|
+
expect(cache?.cache_size).toBe(-65536);
|
|
139
|
+
expect(mmap?.mmap_size).toBe(268435456);
|
|
140
|
+
} finally {
|
|
141
|
+
await database.close();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|