@morlay/rdbkit 0.0.0-stage → 0.0.1-alpha.2
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
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { Pool, types } from "pg";
|
|
3
|
+
import Cursor from "pg-cursor";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
// \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
// src/defs.ts
|
|
17
|
+
/** 声明一张表(形状原样保留,另挂一个取句柄的钩子)。 */
|
|
18
|
+
function defineTable(def) {
|
|
19
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(def.name)) throw new Error(`rdbkit: table name '${def.name}' must match [a-z][a-z0-9_]*`);
|
|
20
|
+
const names = Object.keys(def.columns);
|
|
21
|
+
if (names.length === 0) throw new Error(`rdbkit: table '${def.name}' declares no columns`);
|
|
22
|
+
for (const name of names) if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`rdbkit: table '${def.name}' column '${name}' must match [a-z][a-z0-9_]*`);
|
|
23
|
+
return {
|
|
24
|
+
...def,
|
|
25
|
+
table: (database) => database.table(def)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** 声明一组表。 */
|
|
29
|
+
function defineCatalog(tables) {
|
|
30
|
+
for (const table of tables) defineTable(table);
|
|
31
|
+
return tables;
|
|
32
|
+
}
|
|
33
|
+
// src/dialect.ts
|
|
34
|
+
const SQLITE_KEYWORDS = /^[a-z][a-z0-9_]*$/u;
|
|
35
|
+
function assertIdentifier(kind, name) {
|
|
36
|
+
if (!SQLITE_KEYWORDS.test(name)) throw new Error(`rdbkit: invalid ${kind} '${name}'`);
|
|
37
|
+
}
|
|
38
|
+
function createDialect(name) {
|
|
39
|
+
return name === "sqlite" ? sqliteDialect() : postgresDialect();
|
|
40
|
+
}
|
|
41
|
+
function sqliteDialect() {
|
|
42
|
+
return {
|
|
43
|
+
name: "sqlite",
|
|
44
|
+
quote(identifier) {
|
|
45
|
+
assertIdentifier("identifier", identifier);
|
|
46
|
+
return `"${identifier}"`;
|
|
47
|
+
},
|
|
48
|
+
tableName(table) {
|
|
49
|
+
assertIdentifier("table", table);
|
|
50
|
+
return `"${table}"`;
|
|
51
|
+
},
|
|
52
|
+
columnType(column) {
|
|
53
|
+
if (column.type === "serial") return "INTEGER";
|
|
54
|
+
if (column.type === "boolean") return "INTEGER";
|
|
55
|
+
if (column.type === "bigint" || column.type === "integer") return "INTEGER";
|
|
56
|
+
return "TEXT";
|
|
57
|
+
},
|
|
58
|
+
async readShape(database, table) {
|
|
59
|
+
if ((await database.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [table])).length === 0) return void 0;
|
|
60
|
+
const columns = await database.query(`PRAGMA table_info("${table}")`);
|
|
61
|
+
const indexes = await database.query(`PRAGMA index_list("${table}")`);
|
|
62
|
+
return {
|
|
63
|
+
columns: new Set(columns.map((row) => row.name)),
|
|
64
|
+
indexes: new Set(indexes.map((row) => row.name))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function postgresDialect() {
|
|
70
|
+
return {
|
|
71
|
+
name: "postgres",
|
|
72
|
+
quote(identifier) {
|
|
73
|
+
assertIdentifier("identifier", identifier);
|
|
74
|
+
return `"${identifier}"`;
|
|
75
|
+
},
|
|
76
|
+
tableName(table, schema = "public") {
|
|
77
|
+
assertIdentifier("schema", schema);
|
|
78
|
+
assertIdentifier("table", table);
|
|
79
|
+
return `"${schema}"."${table}"`;
|
|
80
|
+
},
|
|
81
|
+
columnType(column) {
|
|
82
|
+
switch (column.type) {
|
|
83
|
+
case "serial": return "SERIAL";
|
|
84
|
+
case "boolean": return "BOOLEAN";
|
|
85
|
+
case "bigint": return "BIGINT";
|
|
86
|
+
case "integer": return "INTEGER";
|
|
87
|
+
default: return "TEXT";
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async readShape(database, table, schema = "public") {
|
|
91
|
+
const columns = await database.query("SELECT column_name FROM information_schema.columns WHERE table_schema = ? AND table_name = ?", [schema, table]);
|
|
92
|
+
if (columns.length === 0) return void 0;
|
|
93
|
+
const indexes = await database.query("SELECT indexname FROM pg_indexes WHERE schemaname = ? AND tablename = ?", [schema, table]);
|
|
94
|
+
return {
|
|
95
|
+
columns: new Set(columns.map((row) => row.column_name)),
|
|
96
|
+
indexes: new Set(indexes.map((row) => row.indexname))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** 一列的定义 SQL(`"name" TEXT NOT NULL ...`)。 */
|
|
102
|
+
function columnSql$1(dialect, name, column) {
|
|
103
|
+
assertIdentifier("column", name);
|
|
104
|
+
const parts = [dialect.quote(name), dialect.columnType(column)];
|
|
105
|
+
if (column.type === "serial" && dialect.name === "sqlite") parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
106
|
+
else if (column.primaryKey === true) parts.push("PRIMARY KEY");
|
|
107
|
+
if (column.notNull === true) parts.push("NOT NULL");
|
|
108
|
+
if (column.unique === true) parts.push("UNIQUE");
|
|
109
|
+
if (column.default !== void 0) parts.push(`DEFAULT ${literal(dialect, column.default, column.type)}`);
|
|
110
|
+
if (column.references !== void 0) {
|
|
111
|
+
const { table, column: referenced, onDelete } = column.references;
|
|
112
|
+
assertIdentifier("table", table);
|
|
113
|
+
assertIdentifier("column", referenced);
|
|
114
|
+
parts.push(`REFERENCES ${dialect.tableName(table)}(${dialect.quote(referenced)})` + (onDelete === void 0 ? "" : ` ON DELETE ${onDelete.toUpperCase()}`));
|
|
115
|
+
}
|
|
116
|
+
return parts.join(" ");
|
|
117
|
+
}
|
|
118
|
+
function literal(dialect, value, type) {
|
|
119
|
+
if (type === "boolean") {
|
|
120
|
+
if (typeof value === "number") return dialect.name === "postgres" ? value === 0 ? "FALSE" : "TRUE" : String(value);
|
|
121
|
+
return dialect.name === "postgres" ? value.toUpperCase() : /^(true|1)$/iu.test(value) ? "1" : "0";
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "number") return String(value);
|
|
124
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
125
|
+
}
|
|
126
|
+
/** 建表语句(IF NOT EXISTS;列与表级约束都由定义算出来)。 */
|
|
127
|
+
function createTableSql(dialect, table, schema) {
|
|
128
|
+
const parts = Object.entries(table.columns).map(([name, column]) => columnSql$1(dialect, name, column));
|
|
129
|
+
const primaryKey = table.primaryKey === void 0 ? void 0 : table.primaryKey.map((name) => dialect.quote(name)).join(", ");
|
|
130
|
+
if (primaryKey !== void 0) parts.push(`PRIMARY KEY (${primaryKey})`);
|
|
131
|
+
for (const [name, check] of Object.entries(table.checks ?? {})) {
|
|
132
|
+
assertIdentifier("constraint", name);
|
|
133
|
+
parts.push(`CONSTRAINT ${dialect.quote(name)} CHECK (${check.expression})`);
|
|
134
|
+
}
|
|
135
|
+
for (const [name, unique] of Object.entries(table.uniques ?? {})) {
|
|
136
|
+
assertIdentifier("constraint", name);
|
|
137
|
+
parts.push(`CONSTRAINT ${dialect.quote(name)} UNIQUE (${unique.columns.map((column) => dialect.quote(column)).join(", ")})`);
|
|
138
|
+
}
|
|
139
|
+
return `CREATE TABLE IF NOT EXISTS ${dialect.tableName(table.name, schema)} (\n ${parts.join(",\n ")}\n)`;
|
|
140
|
+
}
|
|
141
|
+
/** 索引语句(`unique` 时是唯一索引)。 */
|
|
142
|
+
function createIndexSql(dialect, table, name, columns, unique = false) {
|
|
143
|
+
assertIdentifier("index", name);
|
|
144
|
+
return `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${dialect.quote(name)} ON ${dialect.tableName(table.name)} (${columns.map((column) => dialect.quote(column)).join(", ")})`;
|
|
145
|
+
}
|
|
146
|
+
/** 删列(只在显式允许破坏性变更时用)。 */
|
|
147
|
+
function dropColumnSql(dialect, table, name) {
|
|
148
|
+
assertIdentifier("column", name);
|
|
149
|
+
return `ALTER TABLE ${dialect.tableName(table)} DROP COLUMN ${dialect.quote(name)}`;
|
|
150
|
+
}
|
|
151
|
+
/** 删索引(只在显式允许破坏性变更时用)。 */
|
|
152
|
+
function dropIndexSql(dialect, name) {
|
|
153
|
+
assertIdentifier("index", name);
|
|
154
|
+
return `DROP INDEX IF EXISTS ${dialect.quote(name)}`;
|
|
155
|
+
}
|
|
156
|
+
/** 加列语句(启动迁移只做「缺列则补」,不改类型、不删列)。 */
|
|
157
|
+
function addColumnSql(dialect, table, name, column) {
|
|
158
|
+
return `ALTER TABLE ${dialect.tableName(table)} ADD COLUMN ${columnSql$1(dialect, name, column)}`;
|
|
159
|
+
}
|
|
160
|
+
// src/fragment.ts
|
|
161
|
+
/** 一段纯文本;`only` 限定只在某个方言里出现(SQLite 不支持的行锁等)。 */
|
|
162
|
+
function text(value, options = {}) {
|
|
163
|
+
return () => value === "" ? [] : [{
|
|
164
|
+
text: value,
|
|
165
|
+
...options.only === void 0 ? {} : { only: options.only }
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
/** 一个绑定参数。 */
|
|
169
|
+
function param(value) {
|
|
170
|
+
return () => [{ param: value }];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 把 `?` 占位符与参数列表编成片段(便捷构造;我们生成的 SQL 只含占位符,不含字面量 `?`)。
|
|
174
|
+
*/
|
|
175
|
+
function fragment(sql, params = []) {
|
|
176
|
+
return () => {
|
|
177
|
+
const chunks = sql.split("?");
|
|
178
|
+
const parts = [];
|
|
179
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
180
|
+
if (chunk !== "") parts.push({ text: chunk });
|
|
181
|
+
if (index < params.length) parts.push({ param: params[index] });
|
|
182
|
+
}
|
|
183
|
+
return parts;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** 按顺序拼接(`separator` 只在两个非空片段之间插入,片段内部的各步不拆开)。 */
|
|
187
|
+
function concat(fragments, separator = " ") {
|
|
188
|
+
return () => {
|
|
189
|
+
function* generate() {
|
|
190
|
+
let first = true;
|
|
191
|
+
for (const entry of fragments) {
|
|
192
|
+
const item = typeof entry === "string" ? text(entry) : entry;
|
|
193
|
+
let opened = false;
|
|
194
|
+
for (const part of item()) {
|
|
195
|
+
if (!opened) {
|
|
196
|
+
if (!first && separator !== "") yield { text: separator };
|
|
197
|
+
first = false;
|
|
198
|
+
opened = true;
|
|
199
|
+
}
|
|
200
|
+
yield part;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return generate();
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 把整段限定在某个方言里:每个片段步都带上 `only`,渲染时别的方言整段丢弃
|
|
209
|
+
*(连同它的绑定参数——方言相关表达式靠这一条做到「只留本方言的占位符」)。
|
|
210
|
+
*/
|
|
211
|
+
function only(dialect, source) {
|
|
212
|
+
return () => {
|
|
213
|
+
const parts = [];
|
|
214
|
+
for (const part of source()) parts.push({
|
|
215
|
+
...part,
|
|
216
|
+
only: part.only ?? dialect
|
|
217
|
+
});
|
|
218
|
+
return parts;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** 渲染成某方言的 SQL:SQLite 保持 `?`,PostgreSQL 用 `$1..$n`。 */
|
|
222
|
+
function render(fragment, dialect) {
|
|
223
|
+
const params = [];
|
|
224
|
+
let sql = "";
|
|
225
|
+
for (const part of fragment()) {
|
|
226
|
+
if (part.only !== void 0 && part.only !== dialect) continue;
|
|
227
|
+
if ("text" in part) {
|
|
228
|
+
sql += part.text;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
params.push(part.param);
|
|
232
|
+
sql += dialect === "sqlite" ? "?" : `$${String(params.length)}`;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
text: sql,
|
|
236
|
+
params
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// src/expr.ts
|
|
240
|
+
var expr_exports = /* @__PURE__ */ __exportAll({
|
|
241
|
+
add: () => add,
|
|
242
|
+
always: () => always,
|
|
243
|
+
and: () => and,
|
|
244
|
+
between: () => between,
|
|
245
|
+
coalesce: () => coalesce,
|
|
246
|
+
column: () => column,
|
|
247
|
+
columnSql: () => columnSql,
|
|
248
|
+
countIf: () => countIf,
|
|
249
|
+
eq: () => eq,
|
|
250
|
+
excluded: () => excluded,
|
|
251
|
+
exists: () => exists,
|
|
252
|
+
expr: () => expr,
|
|
253
|
+
gt: () => gt,
|
|
254
|
+
gte: () => gte,
|
|
255
|
+
inList: () => inList,
|
|
256
|
+
inSubquery: () => inSubquery,
|
|
257
|
+
isDistinctFrom: () => isDistinctFrom,
|
|
258
|
+
isNotNull: () => isNotNull,
|
|
259
|
+
isNull: () => isNull,
|
|
260
|
+
jsonInt: () => jsonInt,
|
|
261
|
+
jsonText: () => jsonText,
|
|
262
|
+
like: () => like,
|
|
263
|
+
localDay: () => localDay,
|
|
264
|
+
lower: () => lower,
|
|
265
|
+
lt: () => lt,
|
|
266
|
+
lte: () => lte,
|
|
267
|
+
maxOf: () => maxOf,
|
|
268
|
+
ne: () => ne,
|
|
269
|
+
never: () => never,
|
|
270
|
+
not: () => not,
|
|
271
|
+
notInList: () => notInList,
|
|
272
|
+
notInSubquery: () => notInSubquery,
|
|
273
|
+
notNull: () => notNull,
|
|
274
|
+
operandSql: () => operandSql,
|
|
275
|
+
or: () => or,
|
|
276
|
+
raw: () => raw,
|
|
277
|
+
toInt: () => toInt,
|
|
278
|
+
value: () => value
|
|
279
|
+
});
|
|
280
|
+
function column(name, table) {
|
|
281
|
+
return {
|
|
282
|
+
kind: "column",
|
|
283
|
+
name,
|
|
284
|
+
...table === void 0 ? {} : { table }
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function value(input) {
|
|
288
|
+
return {
|
|
289
|
+
kind: "value",
|
|
290
|
+
value: input
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** 列名引用成 SQL:`"表"."列"` 或 `"列"`。 */
|
|
294
|
+
function columnSql(ref) {
|
|
295
|
+
const name = `"${ref.name}"`;
|
|
296
|
+
return ref.table === void 0 ? name : `"${ref.table}".${name}`;
|
|
297
|
+
}
|
|
298
|
+
/** 把操作数渲染成片段(投影 / 赋值里复用)。 */
|
|
299
|
+
function operandSql(ref) {
|
|
300
|
+
return operand(ref);
|
|
301
|
+
}
|
|
302
|
+
/** 表达式操作数的便捷构造(如 `expr('"f_count" + 1')`)。 */
|
|
303
|
+
function expr(sql, params = []) {
|
|
304
|
+
return {
|
|
305
|
+
kind: "expr",
|
|
306
|
+
sql,
|
|
307
|
+
params
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function asOperand(input) {
|
|
311
|
+
if (typeof input === "object" && input !== null && "kind" in input) return input;
|
|
312
|
+
return value(input);
|
|
313
|
+
}
|
|
314
|
+
function operand(ref) {
|
|
315
|
+
if (ref.kind === "column") return text(columnSql(ref));
|
|
316
|
+
if (ref.kind === "value") return param(ref.value);
|
|
317
|
+
return ref.fragment ?? fragment(ref.sql, ref.params);
|
|
318
|
+
}
|
|
319
|
+
/** 两个方言各给一份片段,由 rdbkit 在渲染时挑本方言的那份。 */
|
|
320
|
+
function dialectExpr(sqlite, postgres) {
|
|
321
|
+
return {
|
|
322
|
+
kind: "expr",
|
|
323
|
+
sql: "",
|
|
324
|
+
params: [],
|
|
325
|
+
fragment: concat([only("sqlite", sqlite), only("postgres", postgres)], "")
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/** 输入转成片段(列 / 值 / 表达式都认)。 */
|
|
329
|
+
function fragmentOf(input) {
|
|
330
|
+
return operand(asOperand(input));
|
|
331
|
+
}
|
|
332
|
+
function binary(left, operator, right) {
|
|
333
|
+
if (right === null) return concat([operand(asOperand(left)), text(`${operator === "=" ? "IS" : "IS NOT"} NULL`)]);
|
|
334
|
+
return concat([
|
|
335
|
+
operand(asOperand(left)),
|
|
336
|
+
text(operator),
|
|
337
|
+
operand(asOperand(right))
|
|
338
|
+
]);
|
|
339
|
+
}
|
|
340
|
+
const eq = (left, right) => binary(left, "=", right);
|
|
341
|
+
const ne = (left, right) => binary(left, "<>", right);
|
|
342
|
+
const gt = (left, right) => binary(left, ">", right);
|
|
343
|
+
const gte = (left, right) => binary(left, ">=", right);
|
|
344
|
+
const lt = (left, right) => binary(left, "<", right);
|
|
345
|
+
const lte = (left, right) => binary(left, "<=", right);
|
|
346
|
+
const like = (left, right) => binary(left, "LIKE", right);
|
|
347
|
+
/** `BETWEEN ? AND ?`。 */
|
|
348
|
+
const between = (ref, low, high) => concat([
|
|
349
|
+
operand(asOperand(ref)),
|
|
350
|
+
text("BETWEEN"),
|
|
351
|
+
operand(asOperand(low)),
|
|
352
|
+
text("AND"),
|
|
353
|
+
operand(asOperand(high))
|
|
354
|
+
]);
|
|
355
|
+
/** `列 IN (子查询)`。 */
|
|
356
|
+
function inSubquery(ref, subquery) {
|
|
357
|
+
return concat([
|
|
358
|
+
operand(asOperand(ref)),
|
|
359
|
+
text("IN ("),
|
|
360
|
+
subquery,
|
|
361
|
+
text(")")
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
/** `列 NOT IN (子查询)`。 */
|
|
365
|
+
function notInSubquery(ref, subquery) {
|
|
366
|
+
return concat([
|
|
367
|
+
operand(asOperand(ref)),
|
|
368
|
+
text("NOT IN ("),
|
|
369
|
+
subquery,
|
|
370
|
+
text(")")
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
/** 恒真条件(占位用,避免业务侧写 `raw("1 = 1")`)。 */
|
|
374
|
+
function always() {
|
|
375
|
+
return raw("1 = 1");
|
|
376
|
+
}
|
|
377
|
+
/** 恒假条件。 */
|
|
378
|
+
function never() {
|
|
379
|
+
return raw("1 = 0");
|
|
380
|
+
}
|
|
381
|
+
/** 函数式表达式:`名字(<参数>)`(值走绑定,参数可以是列 / 值 / 别的表达式)。 */
|
|
382
|
+
function call(name, inputs) {
|
|
383
|
+
return {
|
|
384
|
+
kind: "expr",
|
|
385
|
+
sql: "",
|
|
386
|
+
params: [],
|
|
387
|
+
fragment: concat([
|
|
388
|
+
text(`${name}(`),
|
|
389
|
+
concat(inputs.map((input) => operand(asOperand(input))), ", "),
|
|
390
|
+
text(")")
|
|
391
|
+
], "")
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** `COALESCE(a, b, …)`:值走绑定参数,列/表达式原样渲染。 */
|
|
395
|
+
function coalesce(...inputs) {
|
|
396
|
+
return call("COALESCE", inputs);
|
|
397
|
+
}
|
|
398
|
+
/** `lower(x)`(表达式位置用)。 */
|
|
399
|
+
function lower(input) {
|
|
400
|
+
return call("lower", [input]);
|
|
401
|
+
}
|
|
402
|
+
/** `x IS NOT NULL` 的表达式形式(投影 / 分组位置用)。 */
|
|
403
|
+
function notNull(input) {
|
|
404
|
+
return {
|
|
405
|
+
kind: "expr",
|
|
406
|
+
sql: "",
|
|
407
|
+
params: [],
|
|
408
|
+
fragment: concat([operand(asOperand(input)), text("IS NOT NULL")])
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** `a IS DISTINCT FROM b`(NULL 也参与比较,不会退化成 `IS NULL`)。 */
|
|
412
|
+
function isDistinctFrom(left, right) {
|
|
413
|
+
return concat([
|
|
414
|
+
operand(asOperand(left)),
|
|
415
|
+
text("IS DISTINCT FROM"),
|
|
416
|
+
operand(asOperand(right))
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
/** `EXISTS (子查询)`。 */
|
|
420
|
+
function exists(subquery) {
|
|
421
|
+
return concat([
|
|
422
|
+
text("EXISTS ("),
|
|
423
|
+
subquery,
|
|
424
|
+
text(")")
|
|
425
|
+
]);
|
|
426
|
+
}
|
|
427
|
+
/** `NOT (...)`。 */
|
|
428
|
+
const not = (condition) => concat([
|
|
429
|
+
text("NOT ("),
|
|
430
|
+
condition,
|
|
431
|
+
text(")")
|
|
432
|
+
]);
|
|
433
|
+
/**
|
|
434
|
+
* 显式逃生口:少数表达不了的片段(窗口函数、方言专属函数)才用它。
|
|
435
|
+
* 占位符仍只由 `?` + 参数生成,避免把值拼进文本。
|
|
436
|
+
*/
|
|
437
|
+
function raw(sql, params = []) {
|
|
438
|
+
return fragment(sql, params);
|
|
439
|
+
}
|
|
440
|
+
/** `a + b`(列自增、upsert 累加)。 */
|
|
441
|
+
function add(left, right) {
|
|
442
|
+
return {
|
|
443
|
+
kind: "expr",
|
|
444
|
+
sql: "",
|
|
445
|
+
params: [],
|
|
446
|
+
fragment: concat([
|
|
447
|
+
fragmentOf(left),
|
|
448
|
+
text("+"),
|
|
449
|
+
fragmentOf(right)
|
|
450
|
+
])
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** upsert 冲突时的「新值」引用:`excluded."列"`(只用于 `upsertExpr` 的赋值)。 */
|
|
454
|
+
function excluded(name) {
|
|
455
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`rdbkit: invalid column '${name}'`);
|
|
456
|
+
return {
|
|
457
|
+
kind: "expr",
|
|
458
|
+
sql: "",
|
|
459
|
+
params: [],
|
|
460
|
+
fragment: text(`excluded."${name}"`)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/** 条件渲染成整数列的值(`CASE WHEN cond THEN 1 ELSE 0 END`,两方言同一份)。 */
|
|
464
|
+
function toInt(condition) {
|
|
465
|
+
return {
|
|
466
|
+
kind: "expr",
|
|
467
|
+
sql: "",
|
|
468
|
+
params: [],
|
|
469
|
+
fragment: concat([
|
|
470
|
+
text("CASE WHEN"),
|
|
471
|
+
condition,
|
|
472
|
+
text("THEN 1 ELSE 0 END")
|
|
473
|
+
])
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/** 条件计数:`SUM(CASE WHEN cond THEN 1 ELSE 0 END)`(投影 / 分组位置用)。 */
|
|
477
|
+
function countIf(condition) {
|
|
478
|
+
return {
|
|
479
|
+
kind: "expr",
|
|
480
|
+
sql: "",
|
|
481
|
+
params: [],
|
|
482
|
+
fragment: concat([
|
|
483
|
+
text("SUM("),
|
|
484
|
+
operand(toInt(condition)),
|
|
485
|
+
text(")")
|
|
486
|
+
], "")
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** 只增不减:`max(a, b)` 的方言无关写法(NULL 视作 0,两方言同一份 CASE)。 */
|
|
490
|
+
function maxOf(first, ...rest) {
|
|
491
|
+
let current = fragmentOf(first);
|
|
492
|
+
for (const next of rest) {
|
|
493
|
+
const coalesced = concat([
|
|
494
|
+
text("COALESCE("),
|
|
495
|
+
current,
|
|
496
|
+
text(", 0)")
|
|
497
|
+
], "");
|
|
498
|
+
current = concat([
|
|
499
|
+
text("CASE WHEN"),
|
|
500
|
+
coalesced,
|
|
501
|
+
text("<"),
|
|
502
|
+
fragmentOf(next),
|
|
503
|
+
text("THEN"),
|
|
504
|
+
fragmentOf(next),
|
|
505
|
+
text("ELSE"),
|
|
506
|
+
coalesced,
|
|
507
|
+
text("END")
|
|
508
|
+
]);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
kind: "expr",
|
|
512
|
+
sql: "",
|
|
513
|
+
params: [],
|
|
514
|
+
fragment: current
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** JSON 路径(`$.a.b`)→ 段列表;段只允许标识符字符(PG 侧要内联进 `#>>` 的数组字面量)。 */
|
|
518
|
+
function jsonPathParts(path) {
|
|
519
|
+
if (!path.startsWith("$.") || path.length < 3) throw new Error(`rdbkit: JSON path must look like '$.a.b', got '${path}'`);
|
|
520
|
+
const parts = path.slice(2).split(".");
|
|
521
|
+
for (const part of parts) if (!/^[A-Za-z0-9_]+$/u.test(part)) throw new Error(`rdbkit: unsupported JSON path segment '${part}' in '${path}'`);
|
|
522
|
+
return parts;
|
|
523
|
+
}
|
|
524
|
+
/** JSON 文本提取:SQLite `json_extract(列, 路径)`、PostgreSQL `列::json #>> '{a,b}'`。 */
|
|
525
|
+
function jsonText(input, path) {
|
|
526
|
+
const parts = jsonPathParts(path);
|
|
527
|
+
return dialectExpr(concat([
|
|
528
|
+
text("json_extract("),
|
|
529
|
+
fragmentOf(input),
|
|
530
|
+
text(", "),
|
|
531
|
+
param(path),
|
|
532
|
+
text(")")
|
|
533
|
+
], ""), concat([
|
|
534
|
+
fragmentOf(input),
|
|
535
|
+
text("::json #>> "),
|
|
536
|
+
text(`'{${parts.join(",")}}'`)
|
|
537
|
+
], ""));
|
|
538
|
+
}
|
|
539
|
+
/** JSON 整数提取:文本提取后再转整数(SQLite 的 `json_extract` 本身就按类型给值)。 */
|
|
540
|
+
function jsonInt(input, path) {
|
|
541
|
+
const parts = jsonPathParts(path);
|
|
542
|
+
return dialectExpr(concat([
|
|
543
|
+
text("json_extract("),
|
|
544
|
+
fragmentOf(input),
|
|
545
|
+
text(", "),
|
|
546
|
+
param(path),
|
|
547
|
+
text(")")
|
|
548
|
+
], ""), concat([
|
|
549
|
+
text("("),
|
|
550
|
+
fragmentOf(input),
|
|
551
|
+
text("::json #>> "),
|
|
552
|
+
text(`'{${parts.join(",")}}'`),
|
|
553
|
+
text(")::integer")
|
|
554
|
+
], ""));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 毫秒时间戳 → 本地日(`YYYY-MM-DD`)。
|
|
558
|
+
*
|
|
559
|
+
* 按**数据库所在机器的本地时区**算(SQLite `localtime` / PG 会话时区),两方言口径一致。
|
|
560
|
+
*/
|
|
561
|
+
function localDay(input) {
|
|
562
|
+
return dialectExpr(concat([
|
|
563
|
+
text("date("),
|
|
564
|
+
fragmentOf(input),
|
|
565
|
+
text(" / 1000, 'unixepoch', 'localtime')")
|
|
566
|
+
], ""), concat([
|
|
567
|
+
text("to_char(to_timestamp("),
|
|
568
|
+
fragmentOf(input),
|
|
569
|
+
text(" / 1000.0), 'YYYY-MM-DD')")
|
|
570
|
+
], ""));
|
|
571
|
+
}
|
|
572
|
+
const isNull = (ref) => concat([operand(ref), text("IS NULL")]);
|
|
573
|
+
const isNotNull = (ref) => concat([operand(ref), text("IS NOT NULL")]);
|
|
574
|
+
/** `IN (?, ?, …)`;空列表渲染成恒假(`1 = 0`),避免拼出 `IN ()`。 */
|
|
575
|
+
function inList(left, values) {
|
|
576
|
+
if (values.length === 0) return fragment("1 = 0");
|
|
577
|
+
return concat([operand(asOperand(left)), fragment(`IN (${values.map(() => "?").join(", ")})`, values)]);
|
|
578
|
+
}
|
|
579
|
+
/** `NOT IN (...)`;空列表恒真。 */
|
|
580
|
+
function notInList(left, values) {
|
|
581
|
+
if (values.length === 0) return fragment("1 = 1");
|
|
582
|
+
return concat([operand(asOperand(left)), fragment(`NOT IN (${values.map(() => "?").join(", ")})`, values)]);
|
|
583
|
+
}
|
|
584
|
+
function isEmpty(value) {
|
|
585
|
+
for (const part of value()) return false;
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function combine(keyword, conditions) {
|
|
589
|
+
const parts = conditions.filter((condition) => !isEmpty(condition));
|
|
590
|
+
if (parts.length === 0) return fragment(keyword === "AND" ? "1 = 1" : "1 = 0");
|
|
591
|
+
if (parts.length === 1) return parts[0];
|
|
592
|
+
const flat = parts.flatMap((condition, index) => index === 0 ? [condition] : [text(` ${keyword} `), condition]);
|
|
593
|
+
return concat([
|
|
594
|
+
text("("),
|
|
595
|
+
...flat,
|
|
596
|
+
text(")")
|
|
597
|
+
], "");
|
|
598
|
+
}
|
|
599
|
+
const and = (...conditions) => combine("AND", conditions);
|
|
600
|
+
const or = (...conditions) => combine("OR", conditions);
|
|
601
|
+
// src/compound.ts
|
|
602
|
+
/** 把一条 CTE 拼在 `WITH` 之后(调用方负责逗号)。 */
|
|
603
|
+
function cteSql(cte) {
|
|
604
|
+
const columns = cte.columns === void 0 ? "" : ` (${cte.columns.map((name) => `"${name}"`).join(", ")})`;
|
|
605
|
+
return concat([
|
|
606
|
+
text(`"${cte.name}"${columns} AS (`),
|
|
607
|
+
cte.query,
|
|
608
|
+
text(")")
|
|
609
|
+
], " ");
|
|
610
|
+
}
|
|
611
|
+
/** 组装 `WITH [RECURSIVE] cte, … SELECT …`。 */
|
|
612
|
+
function withCtes(ctes, query) {
|
|
613
|
+
if (ctes.length === 0) return query;
|
|
614
|
+
return concat([
|
|
615
|
+
text(ctes.some((cte) => cte.recursive === true) ? "WITH RECURSIVE" : "WITH"),
|
|
616
|
+
concat(ctes.map(cteSql), ", "),
|
|
617
|
+
query
|
|
618
|
+
], " ");
|
|
619
|
+
}
|
|
620
|
+
/** `UNION` / `UNION ALL`。 */
|
|
621
|
+
function union(left, right, options = {}) {
|
|
622
|
+
return concat([
|
|
623
|
+
left,
|
|
624
|
+
text(options.all === true ? "UNION ALL" : "UNION"),
|
|
625
|
+
right
|
|
626
|
+
], " ");
|
|
627
|
+
}
|
|
628
|
+
/** `INTERSECT`。 */
|
|
629
|
+
function intersect(left, right) {
|
|
630
|
+
return concat([
|
|
631
|
+
left,
|
|
632
|
+
text("INTERSECT"),
|
|
633
|
+
right
|
|
634
|
+
], " ");
|
|
635
|
+
}
|
|
636
|
+
/** `EXCEPT`。 */
|
|
637
|
+
function except(left, right) {
|
|
638
|
+
return concat([
|
|
639
|
+
left,
|
|
640
|
+
text("EXCEPT"),
|
|
641
|
+
right
|
|
642
|
+
], " ");
|
|
643
|
+
}
|
|
644
|
+
/** 行锁:只有 PostgreSQL 认,SQLite 渲染时整段丢掉(分隔符也不能漏出来)。 */
|
|
645
|
+
function forUpdate(options = {}) {
|
|
646
|
+
const parts = ["FOR UPDATE"];
|
|
647
|
+
if (options.of !== void 0 && options.of.length > 0) parts.push(`OF ${options.of.map((name) => `"${name}"`).join(", ")}`);
|
|
648
|
+
if (options.noWait === true) parts.push("NOWAIT");
|
|
649
|
+
if (options.skipLocked === true) parts.push("SKIP LOCKED");
|
|
650
|
+
return text(parts.join(" "), { only: "postgres" });
|
|
651
|
+
}
|
|
652
|
+
/** 表别名(join / CTE 里用)。 */
|
|
653
|
+
function aliasedTable(table, alias) {
|
|
654
|
+
return `"${table.name}" AS "${alias}"`;
|
|
655
|
+
}
|
|
656
|
+
// src/table/select.ts
|
|
657
|
+
/** 表达式的片段:方言相关的表达式自带片段,普通的按 `sql` + 绑定参数编。 */
|
|
658
|
+
function exprFragment(expression) {
|
|
659
|
+
return expression.fragment ?? fragment(expression.sql, expression.params);
|
|
660
|
+
}
|
|
661
|
+
/** COUNT(*),投影里写 `{ n: count() }`。 */
|
|
662
|
+
function count() {
|
|
663
|
+
return {
|
|
664
|
+
kind: "expr",
|
|
665
|
+
sql: "COUNT(*)",
|
|
666
|
+
params: []
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* SUM(列);多表查询时传表名避免歧义。
|
|
671
|
+
*
|
|
672
|
+
* PostgreSQL 的 `SUM(bigint)` 返回 `numeric`(驱动给字符串),SQLite 返回整数——统一 cast 回 int8,
|
|
673
|
+
* 声明的 `number | bigint` 才成立。列类型只有 integer / bigint,所以这个 cast 不丢小数。
|
|
674
|
+
*/
|
|
675
|
+
function sum(name, table) {
|
|
676
|
+
const target = table === void 0 ? `"${name}"` : `"${table}"."${name}"`;
|
|
677
|
+
return {
|
|
678
|
+
kind: "expr",
|
|
679
|
+
sql: "",
|
|
680
|
+
params: [],
|
|
681
|
+
fragment: concat([only("sqlite", text(`SUM(${target})`)), only("postgres", text(`SUM(${target})::bigint`))], "")
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function selectQuery(database, table, projection) {
|
|
685
|
+
const state = {
|
|
686
|
+
joins: [],
|
|
687
|
+
ctes: [],
|
|
688
|
+
distinct: false,
|
|
689
|
+
order: [],
|
|
690
|
+
group: []
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* 结果行的值归一:`bigint` 列统一成 JS `bigint`。
|
|
694
|
+
* SQLite 的 INTEGER 由驱动给 number,PostgreSQL 的 int8 已经是 BigInt(见 `bigintTypes`)——
|
|
695
|
+
* 读出来的类型不能跟着驱动走,否则同一份表定义在两种方言下值类型不同。
|
|
696
|
+
*/
|
|
697
|
+
const normalize = (row) => {
|
|
698
|
+
for (const [alias, entry] of Object.entries(projection)) {
|
|
699
|
+
if (entry.kind !== "column") continue;
|
|
700
|
+
if ((entry.table === void 0 ? table : tableOf(entry.table))?.columns[entry.name]?.type !== "bigint") continue;
|
|
701
|
+
const value = row[alias];
|
|
702
|
+
if (value === null || value === void 0 || typeof value === "bigint") continue;
|
|
703
|
+
row[alias] = BigInt(value);
|
|
704
|
+
}
|
|
705
|
+
return row;
|
|
706
|
+
};
|
|
707
|
+
const tableOf = (name) => {
|
|
708
|
+
if (name === table.name) return table;
|
|
709
|
+
for (const join of state.joins) if ((join.alias ?? join.table.name) === name) return join.table;
|
|
710
|
+
};
|
|
711
|
+
const toFragment = () => {
|
|
712
|
+
const items = Object.entries(projection).map(([alias, entry]) => entry.kind === "column" ? text(`${entry.table === void 0 ? "" : `"${entry.table}".`}"${entry.name}" AS "${alias}"`) : concat([exprFragment(entry), text(`AS "${alias}"`)], " "));
|
|
713
|
+
const parts = [
|
|
714
|
+
state.distinct ? text("SELECT DISTINCT") : text("SELECT"),
|
|
715
|
+
concat(items, ", "),
|
|
716
|
+
text(`FROM "${table.name}"`)
|
|
717
|
+
];
|
|
718
|
+
for (const join of state.joins) {
|
|
719
|
+
const target = join.alias === void 0 ? `"${join.table.name}"` : `"${join.table.name}" AS "${join.alias}"`;
|
|
720
|
+
if (join.kind === "cross") {
|
|
721
|
+
parts.push(text(`CROSS JOIN ${target}`));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const keyword = join.kind === "inner" ? "JOIN" : `${join.kind.toUpperCase()} JOIN`;
|
|
725
|
+
parts.push(text(`${keyword} ${target} ON`), join.on ?? text("1 = 1"));
|
|
726
|
+
}
|
|
727
|
+
if (state.condition !== void 0) parts.push(text("WHERE"), state.condition);
|
|
728
|
+
if (state.group.length > 0) {
|
|
729
|
+
const groupParts = state.group.map((item) => typeof item === "string" ? text(`"${table.name}"."${item}"`) : item.kind === "column" ? text(columnSql(item)) : exprFragment(item));
|
|
730
|
+
parts.push(text("GROUP BY"), concat(groupParts, ", "));
|
|
731
|
+
}
|
|
732
|
+
if (state.having !== void 0) parts.push(text("HAVING"), state.having);
|
|
733
|
+
if (state.order.length > 0) {
|
|
734
|
+
const orderParts = state.order.map((order) => {
|
|
735
|
+
return concat([typeof order.column === "string" ? text(`"${table.name}"."${order.column}"`) : order.column.kind === "column" ? text(columnSql(order.column)) : exprFragment(order.column), text(order.direction.toUpperCase())], " ");
|
|
736
|
+
});
|
|
737
|
+
parts.push(text("ORDER BY"), concat(orderParts, ", "));
|
|
738
|
+
}
|
|
739
|
+
if (state.limit !== void 0) parts.push(fragment("LIMIT ?", [state.limit]));
|
|
740
|
+
if (state.offset !== void 0) parts.push(fragment("OFFSET ?", [state.offset]));
|
|
741
|
+
if (state.lock !== void 0) parts.push(state.lock);
|
|
742
|
+
return withCtes(state.ctes, concat(parts, " "));
|
|
743
|
+
};
|
|
744
|
+
const query = {
|
|
745
|
+
where(condition) {
|
|
746
|
+
state.condition = state.condition === void 0 ? condition : and(state.condition, condition);
|
|
747
|
+
return query;
|
|
748
|
+
},
|
|
749
|
+
join(joined, on, kind = "inner", alias) {
|
|
750
|
+
state.joins.push({
|
|
751
|
+
table: joined,
|
|
752
|
+
on,
|
|
753
|
+
kind,
|
|
754
|
+
alias
|
|
755
|
+
});
|
|
756
|
+
return query;
|
|
757
|
+
},
|
|
758
|
+
with(name, queryText, options = {}) {
|
|
759
|
+
state.ctes.push({
|
|
760
|
+
name,
|
|
761
|
+
query: queryText,
|
|
762
|
+
...options
|
|
763
|
+
});
|
|
764
|
+
return query;
|
|
765
|
+
},
|
|
766
|
+
forUpdate(options) {
|
|
767
|
+
state.lock = forUpdate(options);
|
|
768
|
+
return query;
|
|
769
|
+
},
|
|
770
|
+
groupBy(...columns) {
|
|
771
|
+
state.group.push(...columns);
|
|
772
|
+
return query;
|
|
773
|
+
},
|
|
774
|
+
having(condition) {
|
|
775
|
+
state.having = condition;
|
|
776
|
+
return query;
|
|
777
|
+
},
|
|
778
|
+
distinct() {
|
|
779
|
+
state.distinct = true;
|
|
780
|
+
return query;
|
|
781
|
+
},
|
|
782
|
+
orderBy(column, direction = "asc") {
|
|
783
|
+
state.order.push({
|
|
784
|
+
column,
|
|
785
|
+
direction
|
|
786
|
+
});
|
|
787
|
+
return query;
|
|
788
|
+
},
|
|
789
|
+
limit(count) {
|
|
790
|
+
state.limit = count;
|
|
791
|
+
return query;
|
|
792
|
+
},
|
|
793
|
+
offset(count) {
|
|
794
|
+
state.offset = count;
|
|
795
|
+
return query;
|
|
796
|
+
},
|
|
797
|
+
toFragment,
|
|
798
|
+
async *[Symbol.asyncIterator]() {
|
|
799
|
+
for await (const row of database.iterate(toFragment())) yield normalize(row);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
return query;
|
|
803
|
+
}
|
|
804
|
+
// src/table/render.ts
|
|
805
|
+
function returningClause(returning) {
|
|
806
|
+
return returning === void 0 || returning.length === 0 ? "" : text(`RETURNING ${returning.map((name) => `"${name}"`).join(", ")}`);
|
|
807
|
+
}
|
|
808
|
+
function insertOf(table, row, mode, conflict, update, returning) {
|
|
809
|
+
const columns = Object.keys(row).filter((name) => table.columns[name] !== void 0);
|
|
810
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
811
|
+
const base = fragment(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, columns.map((name) => row[name]));
|
|
812
|
+
if (mode === "insert") return concat([base, returningClause(returning)], " ");
|
|
813
|
+
const target = (conflict ?? []).map((name) => `"${name}"`).join(", ");
|
|
814
|
+
if (mode === "ignore") return concat([base, `ON CONFLICT (${target}) DO NOTHING`], " ");
|
|
815
|
+
return concat([
|
|
816
|
+
base,
|
|
817
|
+
`ON CONFLICT (${target}) ${(update ?? []).length === 0 ? "DO NOTHING" : `DO UPDATE SET ${(update ?? []).map((name) => `"${name}" = excluded."${name}"`).join(", ")}`}`,
|
|
818
|
+
returningClause(returning)
|
|
819
|
+
], " ");
|
|
820
|
+
}
|
|
821
|
+
function insertManyOf(table, rows, options = {}) {
|
|
822
|
+
if (rows.length === 0) return fragment("SELECT 1 WHERE 1 = 0");
|
|
823
|
+
const columns = Object.keys(rows[0]).filter((name) => table.columns[name] !== void 0);
|
|
824
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
825
|
+
for (const row of rows) for (const column of columns) if (row[column] === void 0) throw new Error(`rdbkit: insertMany into "${table.name}" is missing column '${column}'`);
|
|
826
|
+
const tuple = `(${columns.map(() => "?").join(", ")})`;
|
|
827
|
+
return concat([fragment(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) VALUES ${rows.map(() => tuple).join(", ")}`, rows.flatMap((row) => columns.map((name) => row[name]))), conflictClause(options)], " ");
|
|
828
|
+
}
|
|
829
|
+
function insertFromSelectOf(table, columns, source, options = {}) {
|
|
830
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
831
|
+
return concat([
|
|
832
|
+
text(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")})`),
|
|
833
|
+
source,
|
|
834
|
+
conflictClause(options)
|
|
835
|
+
], " ");
|
|
836
|
+
}
|
|
837
|
+
/** `ON CONFLICT …` 子句(没有冲突列时为空)。 */
|
|
838
|
+
function conflictClause(options) {
|
|
839
|
+
const conflict = options.conflict ?? [];
|
|
840
|
+
if (conflict.length === 0) return "";
|
|
841
|
+
return text(`ON CONFLICT (${conflict.map((name) => `"${name}"`).join(", ")}) ${(options.update ?? []).length === 0 ? "DO NOTHING" : `DO UPDATE SET ${(options.update ?? []).map((name) => `"${name}" = excluded."${name}"`).join(", ")}`}`);
|
|
842
|
+
}
|
|
843
|
+
function upsertExprOf(table, row, conflict, assignments) {
|
|
844
|
+
const base = insertOf(table, row, "insert");
|
|
845
|
+
const target = conflict.map((name) => `"${name}"`).join(", ");
|
|
846
|
+
const sets = Object.entries(assignments).map(([name, ref]) => concat([text(`"${name}" =`), operandSql(ref)], " "));
|
|
847
|
+
return concat([
|
|
848
|
+
base,
|
|
849
|
+
text(`ON CONFLICT (${target}) DO UPDATE SET`),
|
|
850
|
+
concat(sets, ", ")
|
|
851
|
+
], " ");
|
|
852
|
+
}
|
|
853
|
+
function updateOf(table, patch, condition, returning) {
|
|
854
|
+
const columns = Object.keys(patch).filter((name) => table.columns[name] !== void 0);
|
|
855
|
+
if (columns.length === 0) throw new Error(`rdbkit: update "${table.name}" has no columns`);
|
|
856
|
+
const sets = columns.map((name) => {
|
|
857
|
+
const value = patch[name];
|
|
858
|
+
const assigned = typeof value === "object" && value !== null && "kind" in value ? operandSql(value) : param(value);
|
|
859
|
+
return concat([text(`"${name}" =`), assigned], " ");
|
|
860
|
+
});
|
|
861
|
+
const head = concat([text(`UPDATE "${table.name}" SET`), concat(sets, ", ")], " ");
|
|
862
|
+
return concat([condition === void 0 ? head : concat([
|
|
863
|
+
head,
|
|
864
|
+
text("WHERE"),
|
|
865
|
+
condition
|
|
866
|
+
], " "), returningClause(returning)], " ");
|
|
867
|
+
}
|
|
868
|
+
function deleteOf(table, condition, returning) {
|
|
869
|
+
const head = text(`DELETE FROM "${table.name}"`);
|
|
870
|
+
return concat([condition === void 0 ? head : concat([
|
|
871
|
+
head,
|
|
872
|
+
text("WHERE"),
|
|
873
|
+
condition
|
|
874
|
+
], " "), returningClause(returning)], " ");
|
|
875
|
+
}
|
|
876
|
+
// src/table/handle.ts
|
|
877
|
+
function createTableHandle(database, definition) {
|
|
878
|
+
const name = definition.name;
|
|
879
|
+
return new Proxy({
|
|
880
|
+
name,
|
|
881
|
+
definition,
|
|
882
|
+
column: (column) => ({
|
|
883
|
+
kind: "column",
|
|
884
|
+
table: name,
|
|
885
|
+
name: column
|
|
886
|
+
}),
|
|
887
|
+
select: (projection) => selectQuery(database, definition, projection ?? Object.fromEntries(Object.keys(definition.columns).map((name) => [name, {
|
|
888
|
+
kind: "column",
|
|
889
|
+
table: definition.name,
|
|
890
|
+
name
|
|
891
|
+
}]))),
|
|
892
|
+
async insert(row, returning) {
|
|
893
|
+
return await database.write(insertOf(definition, row, "insert", void 0, void 0, returning));
|
|
894
|
+
},
|
|
895
|
+
async insertIgnore(row, conflict) {
|
|
896
|
+
await database.run(insertOf(definition, row, "ignore", conflict));
|
|
897
|
+
},
|
|
898
|
+
async insertFromSelect(columns, source, options) {
|
|
899
|
+
await database.run(insertFromSelectOf(definition, columns, source, options));
|
|
900
|
+
},
|
|
901
|
+
async insertMany(rows, options) {
|
|
902
|
+
await database.run(insertManyOf(definition, rows, options));
|
|
903
|
+
},
|
|
904
|
+
async upsert(row, conflict, update) {
|
|
905
|
+
await database.run(insertOf(definition, row, "upsert", conflict, update));
|
|
906
|
+
},
|
|
907
|
+
async update(patch, condition, returning) {
|
|
908
|
+
return await database.write(updateOf(definition, patch, condition, returning));
|
|
909
|
+
},
|
|
910
|
+
async upsertExpr(row, conflict, assignments) {
|
|
911
|
+
await database.run(upsertExprOf(definition, row, conflict, assignments));
|
|
912
|
+
},
|
|
913
|
+
async delete(condition, returning) {
|
|
914
|
+
if (returning !== void 0 && returning.length > 0) return await database.write(deleteOf(definition, condition, returning));
|
|
915
|
+
await database.run(deleteOf(definition, condition));
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
}, { get(target, property, receiver) {
|
|
919
|
+
if (typeof property === "string" && definition.columns[property] !== void 0) return {
|
|
920
|
+
kind: "column",
|
|
921
|
+
table: name,
|
|
922
|
+
name: property
|
|
923
|
+
};
|
|
924
|
+
return Reflect.get(target, property, receiver);
|
|
925
|
+
} });
|
|
926
|
+
}
|
|
927
|
+
// src/catalog.ts
|
|
928
|
+
/** 把表定义列表编成索引(重复表名 fail loud)。 */
|
|
929
|
+
function indexCatalog(catalog) {
|
|
930
|
+
const tables = /* @__PURE__ */ new Map();
|
|
931
|
+
for (const table of catalog) {
|
|
932
|
+
if (tables.has(table.name)) throw new Error(`rdbkit: duplicate table '${table.name}' in catalog`);
|
|
933
|
+
tables.set(table.name, table);
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
get: (name) => tables.get(name),
|
|
937
|
+
has: (name) => tables.has(name),
|
|
938
|
+
names: () => [...tables.keys()]
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
// src/error.ts
|
|
942
|
+
/** 统一的数据库错误:`kind` 是方言无关的判定,原始错误在 `cause`。 */
|
|
943
|
+
var DbError = class extends Error {
|
|
944
|
+
kind;
|
|
945
|
+
constructor(kind, message, options) {
|
|
946
|
+
super(message, options);
|
|
947
|
+
this.kind = kind;
|
|
948
|
+
this.name = "DbError";
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* 把驱动错误翻译成 {@link DbErrorKind}:SQLite 用错误文本前缀,PostgreSQL 用 SQLSTATE。
|
|
953
|
+
* @param error - 驱动抛出的错误。
|
|
954
|
+
* @param dialect - 产生它的方言。
|
|
955
|
+
* @returns 已分类的错误(原错误在 `cause`)。
|
|
956
|
+
*/
|
|
957
|
+
function classifyError(error, dialect) {
|
|
958
|
+
if (error instanceof DbError) return error;
|
|
959
|
+
const cause = error;
|
|
960
|
+
const code = typeof cause?.code === "string" ? cause.code : void 0;
|
|
961
|
+
const message = typeof cause?.message === "string" ? cause.message : String(error);
|
|
962
|
+
if (dialect === "postgres") switch (code) {
|
|
963
|
+
case "23505": return new DbError("unique", message, { cause: error });
|
|
964
|
+
case "23503": return new DbError("foreign-key", message, { cause: error });
|
|
965
|
+
case "23502": return new DbError("not-null", message, { cause: error });
|
|
966
|
+
case "23514": return new DbError("check", message, { cause: error });
|
|
967
|
+
case "57014":
|
|
968
|
+
case "55P03": return new DbError("timeout", message, { cause: error });
|
|
969
|
+
case "53300": return new DbError("connection", message, { cause: error });
|
|
970
|
+
default:
|
|
971
|
+
if (code !== void 0 && code.startsWith("08")) return new DbError("connection", message, { cause: error });
|
|
972
|
+
return new DbError("unknown", message, { cause: error });
|
|
973
|
+
}
|
|
974
|
+
if (/UNIQUE constraint failed/iu.test(message)) return new DbError("unique", message, { cause: error });
|
|
975
|
+
if (/FOREIGN KEY constraint failed/iu.test(message)) return new DbError("foreign-key", message, { cause: error });
|
|
976
|
+
if (/NOT NULL constraint failed/iu.test(message)) return new DbError("not-null", message, { cause: error });
|
|
977
|
+
if (/CHECK constraint failed/iu.test(message)) return new DbError("check", message, { cause: error });
|
|
978
|
+
if (/SQLITE_BUSY|database is locked|database table is locked/iu.test(message)) return new DbError("timeout", message, { cause: error });
|
|
979
|
+
return new DbError("unknown", message, { cause: error });
|
|
980
|
+
}
|
|
981
|
+
// src/database/capabilities.ts
|
|
982
|
+
function assertPragmaName(name) {
|
|
983
|
+
if (name !== "user_version" && name !== "application_id" && name !== "journal_mode") throw new Error(`rdbkit: unsupported pragma '${String(name)}'`);
|
|
984
|
+
}
|
|
985
|
+
/** PRAGMA 不支持绑定参数,值只能内联:只放行整数与标识符形状的字符串。 */
|
|
986
|
+
function assertPragmaValue(name, value) {
|
|
987
|
+
if (typeof value === "number") {
|
|
988
|
+
if (!Number.isSafeInteger(value)) throw new Error(`rdbkit: invalid pragma value '${String(value)}' for '${name}'`);
|
|
989
|
+
return String(value);
|
|
990
|
+
}
|
|
991
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(value)) throw new Error(`rdbkit: invalid pragma value '${value}' for '${name}'`);
|
|
992
|
+
return value;
|
|
993
|
+
}
|
|
994
|
+
/** 现有的表与视图名(迁移探测旧表、拒绝别人的库都要它)。 */
|
|
995
|
+
async function objectNames(database) {
|
|
996
|
+
const query = database.dialect.name === "sqlite" ? fragment("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'") : fragment("SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()");
|
|
997
|
+
return (await database.read(query)).map((row) => row.name);
|
|
998
|
+
}
|
|
999
|
+
/** 回收空间与统计(SQLite `VACUUM` / PostgreSQL `VACUUM ANALYZE`);不得在事务内调用。 */
|
|
1000
|
+
async function vacuum(database) {
|
|
1001
|
+
await database.run(fragment(database.dialect.name === "sqlite" ? "VACUUM" : "VACUUM ANALYZE"));
|
|
1002
|
+
}
|
|
1003
|
+
/** 读 SQLite 的 `PRAGMA`(其它方言直接报不支持)。 */
|
|
1004
|
+
async function sqlitePragma(database, name) {
|
|
1005
|
+
assertPragmaName(name);
|
|
1006
|
+
if (database.dialect.name !== "sqlite") throw new Error(`rdbkit: '${name}' is a SQLite PRAGMA; this database is postgres`);
|
|
1007
|
+
const value = (await database.read(fragment(`PRAGMA ${name}`)))[0]?.[name];
|
|
1008
|
+
return typeof value === "number" || typeof value === "string" ? value : void 0;
|
|
1009
|
+
}
|
|
1010
|
+
/** 写 SQLite 的 `PRAGMA`(值只允许安全字面量,见 `assertPragmaValue`)。 */
|
|
1011
|
+
async function setSqlitePragma(database, name, value) {
|
|
1012
|
+
assertPragmaName(name);
|
|
1013
|
+
if (database.dialect.name !== "sqlite") throw new Error(`rdbkit: '${name}' is a SQLite PRAGMA; this database is postgres`);
|
|
1014
|
+
await database.run(fragment(`PRAGMA ${name} = ${assertPragmaValue(name, value)}`));
|
|
1015
|
+
}
|
|
1016
|
+
// src/database/queue.ts
|
|
1017
|
+
/**
|
|
1018
|
+
* 写通道串行:同一实例的事务与写语句排队执行(SQLite 是单连接,PG 的 serializable 并发会冲突)。
|
|
1019
|
+
* 「当前异步上下文是否已在事务里」由方言层用 `AsyncLocalStorage` 判断(`sqlite.ts` / `postgres.ts`),
|
|
1020
|
+
* 这里只提供队列本身。
|
|
1021
|
+
*/
|
|
1022
|
+
function createSerialQueue() {
|
|
1023
|
+
let tail = Promise.resolve();
|
|
1024
|
+
return (fn) => {
|
|
1025
|
+
const next = tail.then(fn, fn);
|
|
1026
|
+
tail = next.then(() => void 0, () => void 0);
|
|
1027
|
+
return next;
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/** 保存点名:只在同一个事务里唯一即可。 */
|
|
1031
|
+
let savepointCounter = 0;
|
|
1032
|
+
function savepointName() {
|
|
1033
|
+
savepointCounter += 1;
|
|
1034
|
+
return `rdbkit_sp_${savepointCounter}`;
|
|
1035
|
+
}
|
|
1036
|
+
// src/database/postgres.ts
|
|
1037
|
+
/**
|
|
1038
|
+
* PostgreSQL 的 `bigint`(int8)与 `numeric` 默认由驱动给字符串。
|
|
1039
|
+
*
|
|
1040
|
+
* 值是 64 位整数时**不能用 Number 兜**(超过 `2^53` 就悄悄丢精度,`uint64` 的 ID 会撞车),所以:
|
|
1041
|
+
* - `int8`(OID 20)→ JS `BigInt`(精确,对应 Go 的 int64 / uint64);
|
|
1042
|
+
* - `numeric`(OID 1700)保持驱动默认的字符串(可能是定点小数,BigInt 也不对)。
|
|
1043
|
+
*
|
|
1044
|
+
* 业务侧要 number 时自己 `Number(x)`(毫秒时间戳与计数在安全整数内,转换是安全的)。
|
|
1045
|
+
*/
|
|
1046
|
+
function bigintTypes() {
|
|
1047
|
+
const INT8 = 20;
|
|
1048
|
+
return { getTypeParser(oid, format) {
|
|
1049
|
+
if (oid === INT8) return (value) => value === null ? null : BigInt(value);
|
|
1050
|
+
return types.getTypeParser(oid, format);
|
|
1051
|
+
} };
|
|
1052
|
+
}
|
|
1053
|
+
/** PostgreSQL 连接池:返回池上的执行面(游标 + int8 → BigInt)。 */
|
|
1054
|
+
function openPostgres(config) {
|
|
1055
|
+
const pool = new Pool({
|
|
1056
|
+
connectionString: config.connectionString,
|
|
1057
|
+
types: bigintTypes()
|
|
1058
|
+
});
|
|
1059
|
+
const enqueue = createSerialQueue();
|
|
1060
|
+
const current = new AsyncLocalStorage();
|
|
1061
|
+
const cursorRows = async function* (client, text, params) {
|
|
1062
|
+
const cursor = client.query(new Cursor(text, params));
|
|
1063
|
+
try {
|
|
1064
|
+
for (;;) {
|
|
1065
|
+
const rows = await cursor.read(200);
|
|
1066
|
+
if (rows.length === 0) return;
|
|
1067
|
+
for (const row of rows) yield row;
|
|
1068
|
+
}
|
|
1069
|
+
} finally {
|
|
1070
|
+
await cursor.close().catch(() => void 0);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
const pooledIterate = async function* (text, params) {
|
|
1074
|
+
const client = await pool.connect();
|
|
1075
|
+
try {
|
|
1076
|
+
yield* cursorRows(client, text, params);
|
|
1077
|
+
} finally {
|
|
1078
|
+
client.release();
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
/** 一条连接上的执行面(顶层事务与嵌套事务共用)。 */
|
|
1082
|
+
const scopeOf = (client) => ({
|
|
1083
|
+
iterate: (text, params) => cursorRows(client, text, params),
|
|
1084
|
+
async run(text, params) {
|
|
1085
|
+
await client.query(text, params);
|
|
1086
|
+
},
|
|
1087
|
+
transaction(fn) {
|
|
1088
|
+
const name = savepointName();
|
|
1089
|
+
return (async () => {
|
|
1090
|
+
await client.query(`SAVEPOINT ${name}`);
|
|
1091
|
+
try {
|
|
1092
|
+
const result = await current.run(client, () => fn(scopeOf(client)));
|
|
1093
|
+
await client.query(`RELEASE SAVEPOINT ${name}`);
|
|
1094
|
+
return result;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
1097
|
+
await client.query(`RELEASE SAVEPOINT ${name}`);
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
1100
|
+
})();
|
|
1101
|
+
},
|
|
1102
|
+
async close() {}
|
|
1103
|
+
});
|
|
1104
|
+
return {
|
|
1105
|
+
iterate: pooledIterate,
|
|
1106
|
+
run(text, params) {
|
|
1107
|
+
const client = current.getStore();
|
|
1108
|
+
if (client !== void 0) return client.query(text, params).then(() => void 0);
|
|
1109
|
+
return enqueue(async () => {
|
|
1110
|
+
await pool.query(text, params);
|
|
1111
|
+
});
|
|
1112
|
+
},
|
|
1113
|
+
transaction(fn) {
|
|
1114
|
+
const held = current.getStore();
|
|
1115
|
+
if (held !== void 0) return scopeOf(held).transaction(fn);
|
|
1116
|
+
return enqueue(async () => {
|
|
1117
|
+
const client = await pool.connect();
|
|
1118
|
+
try {
|
|
1119
|
+
await client.query(config.isolationLevel === void 0 ? "BEGIN" : `BEGIN ISOLATION LEVEL ${config.isolationLevel.toUpperCase()}`);
|
|
1120
|
+
const result = await current.run(client, () => fn(scopeOf(client)));
|
|
1121
|
+
await client.query("COMMIT");
|
|
1122
|
+
return result;
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
await client.query("ROLLBACK");
|
|
1125
|
+
throw error;
|
|
1126
|
+
} finally {
|
|
1127
|
+
client.release();
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
},
|
|
1131
|
+
async close() {
|
|
1132
|
+
await enqueue(async () => void 0);
|
|
1133
|
+
await pool.end();
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
// src/database/sqlite.ts
|
|
1138
|
+
/** SQLite 连接调优:打开即设 PRAGMA,返回这条连接上的执行面。 */
|
|
1139
|
+
function openSqlite(config) {
|
|
1140
|
+
const client = new DatabaseSync(config.path);
|
|
1141
|
+
client.exec("PRAGMA foreign_keys = ON");
|
|
1142
|
+
client.exec(`PRAGMA busy_timeout = ${config.busyTimeout ?? 5e3}`);
|
|
1143
|
+
if (config.cacheSizeKb !== void 0) client.exec(`PRAGMA cache_size = -${config.cacheSizeKb}`);
|
|
1144
|
+
if (config.mmapSize !== void 0) client.exec(`PRAGMA mmap_size = ${config.mmapSize}`);
|
|
1145
|
+
if (config.journalMode !== void 0 && config.applyJournalMode !== false) client.exec(`PRAGMA journal_mode = ${config.journalMode}`);
|
|
1146
|
+
const enqueue = createSerialQueue();
|
|
1147
|
+
const current = new AsyncLocalStorage();
|
|
1148
|
+
const executor = {
|
|
1149
|
+
async *iterate(text, params) {
|
|
1150
|
+
for (const row of client.prepare(text).iterate(...params)) yield row;
|
|
1151
|
+
},
|
|
1152
|
+
run(text, params) {
|
|
1153
|
+
const write = async () => {
|
|
1154
|
+
client.prepare(text).run(...params);
|
|
1155
|
+
};
|
|
1156
|
+
return current.getStore() === true ? write() : enqueue(write);
|
|
1157
|
+
},
|
|
1158
|
+
transaction(fn) {
|
|
1159
|
+
if (current.getStore() === true) return (async () => {
|
|
1160
|
+
const name = savepointName();
|
|
1161
|
+
client.exec(`SAVEPOINT ${name}`);
|
|
1162
|
+
try {
|
|
1163
|
+
const result = await current.run(true, () => fn(executor));
|
|
1164
|
+
client.exec(`RELEASE SAVEPOINT ${name}`);
|
|
1165
|
+
return result;
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
client.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
1168
|
+
client.exec(`RELEASE SAVEPOINT ${name}`);
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
})();
|
|
1172
|
+
return enqueue(async () => {
|
|
1173
|
+
client.exec("BEGIN IMMEDIATE");
|
|
1174
|
+
try {
|
|
1175
|
+
const result = await current.run(true, () => fn(executor));
|
|
1176
|
+
client.exec("COMMIT");
|
|
1177
|
+
return result;
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
client.exec("ROLLBACK");
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
},
|
|
1184
|
+
async close() {
|
|
1185
|
+
await enqueue(async () => void 0);
|
|
1186
|
+
client.close();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
return executor;
|
|
1190
|
+
}
|
|
1191
|
+
// src/database/create.ts
|
|
1192
|
+
/** 打开时的 catalog:按表名建一次索引(之后 `table("名字")` 才解析得到)。 */
|
|
1193
|
+
function withCatalog(config) {
|
|
1194
|
+
return indexCatalog(config.catalog ?? []);
|
|
1195
|
+
}
|
|
1196
|
+
/** 打开一个库;这是连接创建的唯一入口(实际 connect 在 `sqlite.ts` / `postgres.ts`,业务只拿 `Database`)。 */
|
|
1197
|
+
async function openDatabase(config) {
|
|
1198
|
+
const catalog = withCatalog(config);
|
|
1199
|
+
const executor = config.dialect === "sqlite" ? openSqlite(config) : openPostgres(config);
|
|
1200
|
+
return createDatabase(createDialect(config.dialect), executor, catalog, config.onQuery);
|
|
1201
|
+
}
|
|
1202
|
+
async function* iterateWrapped(rows, dialect) {
|
|
1203
|
+
try {
|
|
1204
|
+
for await (const row of rows) yield row;
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
throw classifyError(error, dialect);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function createDatabase(dialect, executor, catalog, onQuery) {
|
|
1210
|
+
const observe = async (query, run) => {
|
|
1211
|
+
const { text, params } = render(query, dialect.name);
|
|
1212
|
+
const started = performance.now();
|
|
1213
|
+
try {
|
|
1214
|
+
const result = await run(text, params);
|
|
1215
|
+
onQuery?.({
|
|
1216
|
+
dialect: dialect.name,
|
|
1217
|
+
sql: text,
|
|
1218
|
+
params,
|
|
1219
|
+
durationMs: performance.now() - started
|
|
1220
|
+
});
|
|
1221
|
+
return result;
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
onQuery?.({
|
|
1224
|
+
dialect: dialect.name,
|
|
1225
|
+
sql: text,
|
|
1226
|
+
params,
|
|
1227
|
+
durationMs: performance.now() - started,
|
|
1228
|
+
error
|
|
1229
|
+
});
|
|
1230
|
+
throw classifyError(error, dialect.name);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
const database = {
|
|
1234
|
+
dialect,
|
|
1235
|
+
catalog,
|
|
1236
|
+
table: (source) => {
|
|
1237
|
+
const definition = typeof source === "string" ? catalog.get(source) : source;
|
|
1238
|
+
if (definition === void 0) throw new Error(`rdbkit: no table '${typeof source === "string" ? source : source.name}' in catalog`);
|
|
1239
|
+
return createTableHandle(database, definition);
|
|
1240
|
+
},
|
|
1241
|
+
iterate(query) {
|
|
1242
|
+
const { text, params } = render(query, dialect.name);
|
|
1243
|
+
return iterateWrapped(executor.iterate(text, params), dialect.name);
|
|
1244
|
+
},
|
|
1245
|
+
read(query) {
|
|
1246
|
+
return observe(query, async () => {
|
|
1247
|
+
const rows = [];
|
|
1248
|
+
for await (const row of database.iterate(query)) rows.push(row);
|
|
1249
|
+
return rows;
|
|
1250
|
+
});
|
|
1251
|
+
},
|
|
1252
|
+
write(query) {
|
|
1253
|
+
return database.read(query);
|
|
1254
|
+
},
|
|
1255
|
+
run(query) {
|
|
1256
|
+
return observe(query, (text, params) => executor.run(text, params));
|
|
1257
|
+
},
|
|
1258
|
+
transaction(fn) {
|
|
1259
|
+
return executor.transaction((scoped) => fn(createDatabase(dialect, scoped, catalog, onQuery)));
|
|
1260
|
+
},
|
|
1261
|
+
objectNames: () => objectNames(database),
|
|
1262
|
+
vacuum: () => vacuum(database),
|
|
1263
|
+
sqlitePragma: (name) => sqlitePragma(database, name),
|
|
1264
|
+
setSqlitePragma: (name, value) => setSqlitePragma(database, name, value),
|
|
1265
|
+
async close() {
|
|
1266
|
+
await executor.close();
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
return database;
|
|
1270
|
+
}
|
|
1271
|
+
// src/database/wrap.ts
|
|
1272
|
+
/** 把「已有执行面」包成 rdbkit 的 `Database`(宿主自己管连接与事务时用)。 */
|
|
1273
|
+
function wrapDatabase(dialectName, ports) {
|
|
1274
|
+
const database = createDatabase(createDialect(dialectName), {
|
|
1275
|
+
iterate: (text, params) => ports.iterate(text, params),
|
|
1276
|
+
run: (text, params) => ports.run(text, params),
|
|
1277
|
+
transaction: () => {
|
|
1278
|
+
throw new Error("rdbkit: a wrapped database does not own transactions; use the host transaction");
|
|
1279
|
+
},
|
|
1280
|
+
close: async () => {
|
|
1281
|
+
await ports.close?.();
|
|
1282
|
+
}
|
|
1283
|
+
}, indexCatalog([]));
|
|
1284
|
+
return ports.transaction === void 0 ? database : {
|
|
1285
|
+
...database,
|
|
1286
|
+
transaction: ports.transaction
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
/** 结构迁移读取现状用的最小查询面。 */
|
|
1290
|
+
function shapeReader(database) {
|
|
1291
|
+
return { async query(sql, params = []) {
|
|
1292
|
+
return await database.read(fragment(sql, params));
|
|
1293
|
+
} };
|
|
1294
|
+
}
|
|
1295
|
+
// src/migrate.ts
|
|
1296
|
+
/**
|
|
1297
|
+
* 结构差异(纯函数):缺表建表、缺列补列、缺索引补索引。
|
|
1298
|
+
*
|
|
1299
|
+
* 默认只做「增」(缺表建表、缺列补列、缺索引补索引),不改类型、不删索引;删多出来的列只由
|
|
1300
|
+
* `allowDestructive: true` 显式打开(旧库升级默认不丢数据)。
|
|
1301
|
+
* @param dialect - 目标方言。
|
|
1302
|
+
* @param catalog - 表定义(唯一事实来源)。
|
|
1303
|
+
* @param shapes - 现有结构(表名 → 列 / 索引;不存在则 undefined)。
|
|
1304
|
+
* @param options - schema 名。
|
|
1305
|
+
* @returns 需要执行的变更,按依赖顺序(表 → 列 → 索引)。
|
|
1306
|
+
*/
|
|
1307
|
+
function planSchema(dialect, catalog, shapes, options = {}) {
|
|
1308
|
+
const changes = [];
|
|
1309
|
+
for (const table of catalog) {
|
|
1310
|
+
const shape = shapes.get(table.name);
|
|
1311
|
+
if (shape === void 0) changes.push({
|
|
1312
|
+
table: table.name,
|
|
1313
|
+
kind: "create-table",
|
|
1314
|
+
statement: createTableSql(dialect, table, options.schema)
|
|
1315
|
+
});
|
|
1316
|
+
else {
|
|
1317
|
+
for (const [name, column] of Object.entries(table.columns)) {
|
|
1318
|
+
if (shape.columns.has(name)) continue;
|
|
1319
|
+
changes.push({
|
|
1320
|
+
table: table.name,
|
|
1321
|
+
kind: "add-column",
|
|
1322
|
+
statement: addColumnSql(dialect, table.name, name, assertAddable(table.name, name, column))
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (options.allowDestructive === true) for (const name of shape.columns) {
|
|
1326
|
+
if (table.columns[name] !== void 0) continue;
|
|
1327
|
+
changes.push({
|
|
1328
|
+
table: table.name,
|
|
1329
|
+
kind: "drop-column",
|
|
1330
|
+
statement: dropColumnSql(dialect, table.name, name)
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
for (const [name, index] of Object.entries(table.indexes ?? {})) {
|
|
1335
|
+
if (shape?.indexes.has(name) === true) continue;
|
|
1336
|
+
changes.push({
|
|
1337
|
+
table: table.name,
|
|
1338
|
+
kind: "create-index",
|
|
1339
|
+
statement: createIndexSql(dialect, table, name, index.columns)
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
for (const [name, unique] of Object.entries(table.uniques ?? {})) {
|
|
1343
|
+
if (shape?.indexes.has(name) === true) continue;
|
|
1344
|
+
changes.push({
|
|
1345
|
+
table: table.name,
|
|
1346
|
+
kind: "create-index",
|
|
1347
|
+
statement: createIndexSql(dialect, table, name, unique.columns, true)
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return changes;
|
|
1352
|
+
}
|
|
1353
|
+
function assertAddable(table, name, column) {
|
|
1354
|
+
if (column.notNull === true && column.default === void 0) throw new Error(`rdbkit: cannot add NOT NULL column '${table}.${name}' without a default to an existing table`);
|
|
1355
|
+
return column;
|
|
1356
|
+
}
|
|
1357
|
+
/** 让库结构与表定义对齐,并返回实际执行的语句。 */
|
|
1358
|
+
async function ensureSchema(database, catalog, options = {}) {
|
|
1359
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
1360
|
+
for (const table of catalog) shapes.set(table.name, await database.dialect.readShape(shapeReader(database), table.name, options.schema));
|
|
1361
|
+
const changes = planSchema(database.dialect, catalog, shapes, options);
|
|
1362
|
+
for (const change of changes) {
|
|
1363
|
+
options.onStatement?.(change.statement);
|
|
1364
|
+
await database.run(fragment(change.statement));
|
|
1365
|
+
}
|
|
1366
|
+
return changes.map((change) => change.statement);
|
|
1367
|
+
}
|
|
1368
|
+
// src/migration.ts
|
|
1369
|
+
/** 内置迁移账本:一条已执行的数据迁移一行(`f_id` 唯一)。 */
|
|
1370
|
+
const schemaMigrations = defineTable({
|
|
1371
|
+
name: "t_schema_migrations",
|
|
1372
|
+
columns: {
|
|
1373
|
+
f_id: {
|
|
1374
|
+
type: "text",
|
|
1375
|
+
primaryKey: true,
|
|
1376
|
+
notNull: true
|
|
1377
|
+
},
|
|
1378
|
+
f_applied_at: {
|
|
1379
|
+
type: "bigint",
|
|
1380
|
+
notNull: true
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
/** 迁移注册表:业务把自己的迁移登记进来,打开库时一起执行。 */
|
|
1385
|
+
var MigrationRegistry = class {
|
|
1386
|
+
#migrations = /* @__PURE__ */ new Map();
|
|
1387
|
+
register(migration) {
|
|
1388
|
+
if (this.#migrations.has(migration.id)) throw new Error(`rdbkit: migration '${migration.id}' is registered twice`);
|
|
1389
|
+
this.#migrations.set(migration.id, migration);
|
|
1390
|
+
return this;
|
|
1391
|
+
}
|
|
1392
|
+
list() {
|
|
1393
|
+
return [...this.#migrations.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
/**
|
|
1397
|
+
* 先按 catalog 对齐结构(建表 / 补列 / 补索引),再执行未应用的数据迁移。
|
|
1398
|
+
*
|
|
1399
|
+
* 账本是 rdbkit 内置的 `t_schema_migrations`:每条迁移在**同一个事务**里执行并记账,
|
|
1400
|
+
* 失败即回滚,重跑时只跑没记过的。返回本次实际执行了哪些 `id`。
|
|
1401
|
+
*/
|
|
1402
|
+
async function runMigrations(database, options) {
|
|
1403
|
+
const migrations = options.migrations instanceof MigrationRegistry ? options.migrations.list() : [...options.migrations];
|
|
1404
|
+
await ensureSchema(database, [...options.catalog ?? [], schemaMigrations]);
|
|
1405
|
+
const ledger = database.table(schemaMigrations);
|
|
1406
|
+
const applied = /* @__PURE__ */ new Set();
|
|
1407
|
+
for await (const row of ledger.select({ id: ledger.f_id })) applied.add(row.id);
|
|
1408
|
+
const executed = [];
|
|
1409
|
+
for (const migration of migrations) {
|
|
1410
|
+
if (applied.has(migration.id)) continue;
|
|
1411
|
+
if (await database.transaction(async (tx) => {
|
|
1412
|
+
if (await migration.up(tx) === false) return false;
|
|
1413
|
+
await tx.table(schemaMigrations).insert({
|
|
1414
|
+
f_id: migration.id,
|
|
1415
|
+
f_applied_at: Date.now()
|
|
1416
|
+
});
|
|
1417
|
+
return true;
|
|
1418
|
+
})) executed.push(migration.id);
|
|
1419
|
+
}
|
|
1420
|
+
return executed;
|
|
1421
|
+
}
|
|
1422
|
+
export { DbError, expr_exports as E, MigrationRegistry, add, addColumnSql, aliasedTable, and, between, classifyError, column, columnSql as columnRefSql, columnSql$1 as columnSql, concat, count, countIf, createDialect, createIndexSql, createTableHandle, createTableSql, cteSql, defineCatalog, defineTable, dropColumnSql, dropIndexSql, ensureSchema, eq, except, excluded, exists, expr, forUpdate, fragment, gt, gte, inList, inSubquery, indexCatalog, intersect, isNotNull, isNull, jsonInt, jsonText, like, localDay, lt, lte, maxOf, ne, not, notInList, notInSubquery, openDatabase, operandSql, or, planSchema, raw, render, runMigrations, schemaMigrations, shapeReader, sum, toInt, union, value, withCtes, wrapDatabase };
|