@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
package/src/expr.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { concat, fragment, only, param, text, type Fragment } from "./fragment.ts";
|
|
2
|
+
|
|
3
|
+
/** 列引用:查询构建器统一用双引号(两种方言都认);`__value` 只做类型携带。 */
|
|
4
|
+
export interface ColumnRef<T = unknown> {
|
|
5
|
+
readonly kind: "column";
|
|
6
|
+
readonly table?: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly __value?: T;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 字面参数(值走绑定,不拼字符串)。 */
|
|
12
|
+
export interface Value {
|
|
13
|
+
readonly kind: "value";
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 表达式操作数(聚合、函数调用这类不是列也不是字面量值的东西)。 */
|
|
18
|
+
export interface ExprOperand<T = unknown> {
|
|
19
|
+
readonly kind: "expr";
|
|
20
|
+
readonly sql: string;
|
|
21
|
+
readonly params: readonly unknown[];
|
|
22
|
+
/** 方言相关的表达式(JSON 提取、本地日这类)用片段表达;给了它就不用 `sql` / `params`。 */
|
|
23
|
+
readonly fragment?: Fragment;
|
|
24
|
+
readonly __value?: T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 列、值或表达式。 */
|
|
28
|
+
export type Operand = ColumnRef | Value | ExprOperand;
|
|
29
|
+
|
|
30
|
+
/** 条件里可以直接写原始值(自动包成绑定参数);`null` 走 IS NULL / IS NOT NULL。 */
|
|
31
|
+
export type OperandInput = Operand | string | number | boolean | bigint | null;
|
|
32
|
+
|
|
33
|
+
/** 一个条件(可再组合的片段)。 */
|
|
34
|
+
export type Condition = Fragment;
|
|
35
|
+
|
|
36
|
+
export function column<T = unknown>(name: string, table?: string): ColumnRef<T> {
|
|
37
|
+
return { kind: "column", name, ...(table === undefined ? {} : { table }) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function value(input: unknown): Value {
|
|
41
|
+
return { kind: "value", value: input };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 列名引用成 SQL:`"表"."列"` 或 `"列"`。 */
|
|
45
|
+
export function columnSql(ref: ColumnRef): string {
|
|
46
|
+
const name = `"${ref.name}"`;
|
|
47
|
+
return ref.table === undefined ? name : `"${ref.table}".${name}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 把操作数渲染成片段(投影 / 赋值里复用)。 */
|
|
51
|
+
export function operandSql(ref: Operand): Fragment {
|
|
52
|
+
return operand(ref);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 表达式操作数的便捷构造(如 `expr('"f_count" + 1')`)。 */
|
|
56
|
+
export function expr(sql: string, params: readonly unknown[] = []): ExprOperand {
|
|
57
|
+
return { kind: "expr", sql, params };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function asOperand(input: OperandInput): Operand {
|
|
61
|
+
if (typeof input === "object" && input !== null && "kind" in input) return input;
|
|
62
|
+
return value(input);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function operand(ref: Operand): Fragment {
|
|
66
|
+
if (ref.kind === "column") return text(columnSql(ref));
|
|
67
|
+
if (ref.kind === "value") return param(ref.value);
|
|
68
|
+
return ref.fragment ?? fragment(ref.sql, ref.params);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 两个方言各给一份片段,由 rdbkit 在渲染时挑本方言的那份。 */
|
|
72
|
+
function dialectExpr<T>(sqlite: Fragment, postgres: Fragment): ExprOperand<T> {
|
|
73
|
+
return {
|
|
74
|
+
kind: "expr",
|
|
75
|
+
sql: "",
|
|
76
|
+
params: [],
|
|
77
|
+
fragment: concat([only("sqlite", sqlite), only("postgres", postgres)], ""),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 输入转成片段(列 / 值 / 表达式都认)。 */
|
|
82
|
+
function fragmentOf(input: OperandInput): Fragment {
|
|
83
|
+
return operand(asOperand(input));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function binary(left: OperandInput, operator: string, right: OperandInput): Condition {
|
|
87
|
+
if (right === null)
|
|
88
|
+
return concat([operand(asOperand(left)), text(`${operator === "=" ? "IS" : "IS NOT"} NULL`)]);
|
|
89
|
+
return concat([operand(asOperand(left)), text(operator), operand(asOperand(right))]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const eq = (left: OperandInput, right: OperandInput): Condition => binary(left, "=", right);
|
|
93
|
+
export const ne = (left: OperandInput, right: OperandInput): Condition => binary(left, "<>", right);
|
|
94
|
+
export const gt = (left: OperandInput, right: OperandInput): Condition => binary(left, ">", right);
|
|
95
|
+
export const gte = (left: OperandInput, right: OperandInput): Condition =>
|
|
96
|
+
binary(left, ">=", right);
|
|
97
|
+
export const lt = (left: OperandInput, right: OperandInput): Condition => binary(left, "<", right);
|
|
98
|
+
export const lte = (left: OperandInput, right: OperandInput): Condition =>
|
|
99
|
+
binary(left, "<=", right);
|
|
100
|
+
export const like = (left: OperandInput, right: OperandInput): Condition =>
|
|
101
|
+
binary(left, "LIKE", right);
|
|
102
|
+
|
|
103
|
+
/** `BETWEEN ? AND ?`。 */
|
|
104
|
+
export const between = (ref: OperandInput, low: OperandInput, high: OperandInput): Condition =>
|
|
105
|
+
concat([
|
|
106
|
+
operand(asOperand(ref)),
|
|
107
|
+
text("BETWEEN"),
|
|
108
|
+
operand(asOperand(low)),
|
|
109
|
+
text("AND"),
|
|
110
|
+
operand(asOperand(high)),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
/** `列 IN (子查询)`。 */
|
|
114
|
+
export function inSubquery(ref: Operand, subquery: Fragment): Condition {
|
|
115
|
+
return concat([operand(asOperand(ref)), text("IN ("), subquery, text(")")]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `列 NOT IN (子查询)`。 */
|
|
119
|
+
export function notInSubquery(ref: OperandInput, subquery: Fragment): Condition {
|
|
120
|
+
return concat([operand(asOperand(ref)), text("NOT IN ("), subquery, text(")")]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 恒真条件(占位用,避免业务侧写 `raw("1 = 1")`)。 */
|
|
124
|
+
export function always(): Condition {
|
|
125
|
+
return raw("1 = 1");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 恒假条件。 */
|
|
129
|
+
export function never(): Condition {
|
|
130
|
+
return raw("1 = 0");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 函数式表达式:`名字(<参数>)`(值走绑定,参数可以是列 / 值 / 别的表达式)。 */
|
|
134
|
+
function call(name: string, inputs: readonly OperandInput[]): ExprOperand {
|
|
135
|
+
return {
|
|
136
|
+
kind: "expr",
|
|
137
|
+
sql: "",
|
|
138
|
+
params: [],
|
|
139
|
+
fragment: concat(
|
|
140
|
+
[
|
|
141
|
+
text(`${name}(`),
|
|
142
|
+
concat(
|
|
143
|
+
inputs.map((input) => operand(asOperand(input))),
|
|
144
|
+
", ",
|
|
145
|
+
),
|
|
146
|
+
text(")"),
|
|
147
|
+
],
|
|
148
|
+
"",
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `COALESCE(a, b, …)`:值走绑定参数,列/表达式原样渲染。 */
|
|
154
|
+
export function coalesce(...inputs: OperandInput[]): ExprOperand {
|
|
155
|
+
return call("COALESCE", inputs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `lower(x)`(表达式位置用)。 */
|
|
159
|
+
export function lower(input: OperandInput): ExprOperand {
|
|
160
|
+
return call("lower", [input]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `x IS NOT NULL` 的表达式形式(投影 / 分组位置用)。 */
|
|
164
|
+
export function notNull(input: OperandInput): ExprOperand {
|
|
165
|
+
return {
|
|
166
|
+
kind: "expr",
|
|
167
|
+
sql: "",
|
|
168
|
+
params: [],
|
|
169
|
+
fragment: concat([operand(asOperand(input)), text("IS NOT NULL")]),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `a IS DISTINCT FROM b`(NULL 也参与比较,不会退化成 `IS NULL`)。 */
|
|
174
|
+
export function isDistinctFrom(left: OperandInput, right: OperandInput): Condition {
|
|
175
|
+
return concat([operand(asOperand(left)), text("IS DISTINCT FROM"), operand(asOperand(right))]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `EXISTS (子查询)`。 */
|
|
179
|
+
export function exists(subquery: Fragment): Condition {
|
|
180
|
+
return concat([text("EXISTS ("), subquery, text(")")]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `NOT (...)`。 */
|
|
184
|
+
export const not = (condition: Condition): Condition =>
|
|
185
|
+
concat([text("NOT ("), condition, text(")")]);
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 显式逃生口:少数表达不了的片段(窗口函数、方言专属函数)才用它。
|
|
189
|
+
* 占位符仍只由 `?` + 参数生成,避免把值拼进文本。
|
|
190
|
+
*/
|
|
191
|
+
export function raw(sql: string, params: readonly unknown[] = []): Condition {
|
|
192
|
+
return fragment(sql, params);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `a + b`(列自增、upsert 累加)。 */
|
|
196
|
+
export function add(left: OperandInput, right: OperandInput): ExprOperand<number> {
|
|
197
|
+
return { kind: "expr", sql: "", params: [], fragment: concat([fragmentOf(left), text("+"), fragmentOf(right)]) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** upsert 冲突时的「新值」引用:`excluded."列"`(只用于 `upsertExpr` 的赋值)。 */
|
|
201
|
+
export function excluded(name: string): ExprOperand {
|
|
202
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`rdbkit: invalid column '${name}'`);
|
|
203
|
+
return { kind: "expr", sql: "", params: [], fragment: text(`excluded."${name}"`) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 条件渲染成整数列的值(`CASE WHEN cond THEN 1 ELSE 0 END`,两方言同一份)。 */
|
|
207
|
+
export function toInt(condition: Condition): ExprOperand<number> {
|
|
208
|
+
return {
|
|
209
|
+
kind: "expr",
|
|
210
|
+
sql: "",
|
|
211
|
+
params: [],
|
|
212
|
+
fragment: concat([text("CASE WHEN"), condition, text("THEN 1 ELSE 0 END")]),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 条件计数:`SUM(CASE WHEN cond THEN 1 ELSE 0 END)`(投影 / 分组位置用)。 */
|
|
217
|
+
export function countIf(condition: Condition): ExprOperand<number> {
|
|
218
|
+
return {
|
|
219
|
+
kind: "expr",
|
|
220
|
+
sql: "",
|
|
221
|
+
params: [],
|
|
222
|
+
fragment: concat([text("SUM("), operand(toInt(condition)), text(")")], ""),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 只增不减:`max(a, b)` 的方言无关写法(NULL 视作 0,两方言同一份 CASE)。 */
|
|
227
|
+
export function maxOf(first: OperandInput, ...rest: readonly OperandInput[]): ExprOperand<number> {
|
|
228
|
+
let current = fragmentOf(first);
|
|
229
|
+
for (const next of rest) {
|
|
230
|
+
const coalesced = concat([text("COALESCE("), current, text(", 0)")], "");
|
|
231
|
+
current = concat([
|
|
232
|
+
text("CASE WHEN"),
|
|
233
|
+
coalesced,
|
|
234
|
+
text("<"),
|
|
235
|
+
fragmentOf(next),
|
|
236
|
+
text("THEN"),
|
|
237
|
+
fragmentOf(next),
|
|
238
|
+
text("ELSE"),
|
|
239
|
+
coalesced,
|
|
240
|
+
text("END"),
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
return { kind: "expr", sql: "", params: [], fragment: current };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** JSON 路径(`$.a.b`)→ 段列表;段只允许标识符字符(PG 侧要内联进 `#>>` 的数组字面量)。 */
|
|
247
|
+
function jsonPathParts(path: string): string[] {
|
|
248
|
+
if (!path.startsWith("$.") || path.length < 3) {
|
|
249
|
+
throw new Error(`rdbkit: JSON path must look like '$.a.b', got '${path}'`);
|
|
250
|
+
}
|
|
251
|
+
const parts = path.slice(2).split(".");
|
|
252
|
+
for (const part of parts) {
|
|
253
|
+
if (!/^[A-Za-z0-9_]+$/u.test(part)) {
|
|
254
|
+
throw new Error(`rdbkit: unsupported JSON path segment '${part}' in '${path}'`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return parts;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** JSON 文本提取:SQLite `json_extract(列, 路径)`、PostgreSQL `列::json #>> '{a,b}'`。 */
|
|
261
|
+
export function jsonText(input: OperandInput, path: string): ExprOperand<string | null> {
|
|
262
|
+
const parts = jsonPathParts(path);
|
|
263
|
+
return dialectExpr<string | null>(
|
|
264
|
+
concat([text("json_extract("), fragmentOf(input), text(", "), param(path), text(")")], ""),
|
|
265
|
+
concat([fragmentOf(input), text("::json #>> "), text(`'{${parts.join(",")}}'`)], ""),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** JSON 整数提取:文本提取后再转整数(SQLite 的 `json_extract` 本身就按类型给值)。 */
|
|
270
|
+
export function jsonInt(input: OperandInput, path: string): ExprOperand<number | null> {
|
|
271
|
+
const parts = jsonPathParts(path);
|
|
272
|
+
return dialectExpr<number | null>(
|
|
273
|
+
concat([text("json_extract("), fragmentOf(input), text(", "), param(path), text(")")], ""),
|
|
274
|
+
concat(
|
|
275
|
+
[
|
|
276
|
+
text("("),
|
|
277
|
+
fragmentOf(input),
|
|
278
|
+
text("::json #>> "),
|
|
279
|
+
text(`'{${parts.join(",")}}'`),
|
|
280
|
+
text(")::integer"),
|
|
281
|
+
],
|
|
282
|
+
"",
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 毫秒时间戳 → 本地日(`YYYY-MM-DD`)。
|
|
289
|
+
*
|
|
290
|
+
* 按**数据库所在机器的本地时区**算(SQLite `localtime` / PG 会话时区),两方言口径一致。
|
|
291
|
+
*/
|
|
292
|
+
export function localDay(input: OperandInput): ExprOperand<string> {
|
|
293
|
+
return dialectExpr<string>(
|
|
294
|
+
concat(
|
|
295
|
+
[text("date("), fragmentOf(input), text(" / 1000, 'unixepoch', 'localtime')")],
|
|
296
|
+
"",
|
|
297
|
+
),
|
|
298
|
+
concat(
|
|
299
|
+
[text("to_char(to_timestamp("), fragmentOf(input), text(" / 1000.0), 'YYYY-MM-DD')")],
|
|
300
|
+
"",
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const isNull = (ref: Operand): Condition => concat([operand(ref), text("IS NULL")]);
|
|
306
|
+
export const isNotNull = (ref: Operand): Condition => concat([operand(ref), text("IS NOT NULL")]);
|
|
307
|
+
|
|
308
|
+
/** `IN (?, ?, …)`;空列表渲染成恒假(`1 = 0`),避免拼出 `IN ()`。 */
|
|
309
|
+
export function inList(left: OperandInput, values: readonly unknown[]): Condition {
|
|
310
|
+
if (values.length === 0) return fragment("1 = 0");
|
|
311
|
+
return concat([
|
|
312
|
+
operand(asOperand(left)),
|
|
313
|
+
fragment(`IN (${values.map(() => "?").join(", ")})`, values),
|
|
314
|
+
]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `NOT IN (...)`;空列表恒真。 */
|
|
318
|
+
export function notInList(left: OperandInput, values: readonly unknown[]): Condition {
|
|
319
|
+
if (values.length === 0) return fragment("1 = 1");
|
|
320
|
+
return concat([
|
|
321
|
+
operand(asOperand(left)),
|
|
322
|
+
fragment(`NOT IN (${values.map(() => "?").join(", ")})`, values),
|
|
323
|
+
]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isEmpty(value: Fragment): boolean {
|
|
327
|
+
for (const part of value()) {
|
|
328
|
+
void part;
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function combine(keyword: "AND" | "OR", conditions: readonly Condition[]): Condition {
|
|
335
|
+
const parts = conditions.filter((condition) => !isEmpty(condition));
|
|
336
|
+
if (parts.length === 0) return fragment(keyword === "AND" ? "1 = 1" : "1 = 0");
|
|
337
|
+
if (parts.length === 1) return parts[0]!;
|
|
338
|
+
// 多个条件一律加括号:`a AND b OR c` 的优先级容易在组合时出错。
|
|
339
|
+
const flat = parts.flatMap((condition, index) =>
|
|
340
|
+
index === 0 ? [condition] : [text(` ${keyword} `), condition],
|
|
341
|
+
);
|
|
342
|
+
return concat([text("("), ...flat, text(")")], "");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const and = (...conditions: readonly Condition[]): Condition => combine("AND", conditions);
|
|
346
|
+
export const or = (...conditions: readonly Condition[]): Condition => combine("OR", conditions);
|
package/src/fragment.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { DialectName } from "./dialect.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 片段里的一步:要么是一段 SQL 文本,要么是一个绑定参数。
|
|
5
|
+
* 参数不在片段里决定占位符——渲染时按方言给 `?` 或 `$n`。
|
|
6
|
+
*/
|
|
7
|
+
export type FragPart =
|
|
8
|
+
| { readonly text: string; readonly only?: DialectName }
|
|
9
|
+
| { readonly param: unknown; readonly only?: DialectName };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SQL 片段是**惰性的迭代器**(与 Golang `sqlfrag` 的 `iter.Seq2[string, []any]` 对齐):
|
|
13
|
+
* 组合时不拼字符串,只有渲染 / 执行时才逐段展开。可重复消费(每次调用重新生成)。
|
|
14
|
+
*/
|
|
15
|
+
export type Fragment = () => Iterable<FragPart>;
|
|
16
|
+
|
|
17
|
+
/** 一段纯文本;`only` 限定只在某个方言里出现(SQLite 不支持的行锁等)。 */
|
|
18
|
+
export function text(value: string, options: { only?: DialectName } = {}): Fragment {
|
|
19
|
+
return () =>
|
|
20
|
+
value === ""
|
|
21
|
+
? []
|
|
22
|
+
: [{ text: value, ...(options.only === undefined ? {} : { only: options.only }) }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 一个绑定参数。 */
|
|
26
|
+
export function param(value: unknown): Fragment {
|
|
27
|
+
return () => [{ param: value }];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 把 `?` 占位符与参数列表编成片段(便捷构造;我们生成的 SQL 只含占位符,不含字面量 `?`)。
|
|
32
|
+
*/
|
|
33
|
+
export function fragment(sql: string, params: readonly unknown[] = []): Fragment {
|
|
34
|
+
return () => {
|
|
35
|
+
const chunks = sql.split("?");
|
|
36
|
+
const parts: FragPart[] = [];
|
|
37
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
38
|
+
if (chunk !== "") parts.push({ text: chunk });
|
|
39
|
+
if (index < params.length) parts.push({ param: params[index] });
|
|
40
|
+
}
|
|
41
|
+
return parts;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 按顺序拼接(`separator` 只在两个非空片段之间插入,片段内部的各步不拆开)。 */
|
|
46
|
+
export function concat(fragments: readonly (Fragment | string)[], separator = " "): Fragment {
|
|
47
|
+
return () => {
|
|
48
|
+
function* generate(): Generator<FragPart> {
|
|
49
|
+
let first = true;
|
|
50
|
+
for (const entry of fragments) {
|
|
51
|
+
const item = typeof entry === "string" ? text(entry) : entry;
|
|
52
|
+
let opened = false;
|
|
53
|
+
for (const part of item()) {
|
|
54
|
+
if (!opened) {
|
|
55
|
+
if (!first && separator !== "") yield { text: separator };
|
|
56
|
+
first = false;
|
|
57
|
+
opened = true;
|
|
58
|
+
}
|
|
59
|
+
yield part;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return generate();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 把整段限定在某个方言里:每个片段步都带上 `only`,渲染时别的方言整段丢弃
|
|
69
|
+
*(连同它的绑定参数——方言相关表达式靠这一条做到「只留本方言的占位符」)。
|
|
70
|
+
*/
|
|
71
|
+
export function only(dialect: DialectName, source: Fragment): Fragment {
|
|
72
|
+
return () => {
|
|
73
|
+
const parts: FragPart[] = [];
|
|
74
|
+
for (const part of source()) parts.push({ ...part, only: part.only ?? dialect });
|
|
75
|
+
return parts;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 渲染成某方言的 SQL:SQLite 保持 `?`,PostgreSQL 用 `$1..$n`。 */
|
|
80
|
+
export function render(
|
|
81
|
+
fragment: Fragment,
|
|
82
|
+
dialect: DialectName,
|
|
83
|
+
): { text: string; params: unknown[] } {
|
|
84
|
+
const params: unknown[] = [];
|
|
85
|
+
let sql = "";
|
|
86
|
+
for (const part of fragment()) {
|
|
87
|
+
if (part.only !== undefined && part.only !== dialect) continue;
|
|
88
|
+
if ("text" in part) {
|
|
89
|
+
sql += part.text;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
params.push(part.param);
|
|
93
|
+
sql += dialect === "sqlite" ? "?" : `$${String(params.length)}`;
|
|
94
|
+
}
|
|
95
|
+
return { text: sql, params };
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export {
|
|
2
|
+
defineCatalog,
|
|
3
|
+
defineTable,
|
|
4
|
+
type Catalog,
|
|
5
|
+
type CheckDef,
|
|
6
|
+
type ColumnDef,
|
|
7
|
+
type ColumnTypeName,
|
|
8
|
+
type DeleteAction,
|
|
9
|
+
type IndexDef,
|
|
10
|
+
type Table,
|
|
11
|
+
type TableDef,
|
|
12
|
+
type UniqueDef,
|
|
13
|
+
} from "./defs.ts";
|
|
14
|
+
export {
|
|
15
|
+
addColumnSql,
|
|
16
|
+
columnSql,
|
|
17
|
+
createDialect,
|
|
18
|
+
createIndexSql,
|
|
19
|
+
createTableSql,
|
|
20
|
+
type Dialect,
|
|
21
|
+
type DialectName,
|
|
22
|
+
type TableShape,
|
|
23
|
+
} from "./dialect.ts";
|
|
24
|
+
export { concat, fragment, render, type Fragment } from "./fragment.ts";
|
|
25
|
+
export {
|
|
26
|
+
and,
|
|
27
|
+
between,
|
|
28
|
+
column,
|
|
29
|
+
columnSql as columnRefSql,
|
|
30
|
+
eq,
|
|
31
|
+
exists,
|
|
32
|
+
expr,
|
|
33
|
+
excluded,
|
|
34
|
+
add,
|
|
35
|
+
countIf,
|
|
36
|
+
gt,
|
|
37
|
+
gte,
|
|
38
|
+
inList,
|
|
39
|
+
inSubquery,
|
|
40
|
+
isNotNull,
|
|
41
|
+
isNull,
|
|
42
|
+
jsonInt,
|
|
43
|
+
jsonText,
|
|
44
|
+
like,
|
|
45
|
+
localDay,
|
|
46
|
+
maxOf,
|
|
47
|
+
lt,
|
|
48
|
+
lte,
|
|
49
|
+
ne,
|
|
50
|
+
not,
|
|
51
|
+
notInList,
|
|
52
|
+
notInSubquery,
|
|
53
|
+
or,
|
|
54
|
+
operandSql,
|
|
55
|
+
raw,
|
|
56
|
+
toInt,
|
|
57
|
+
value,
|
|
58
|
+
type ColumnRef,
|
|
59
|
+
type Condition,
|
|
60
|
+
type ExprOperand,
|
|
61
|
+
type Operand,
|
|
62
|
+
type OperandInput,
|
|
63
|
+
} from "./expr.ts";
|
|
64
|
+
export {
|
|
65
|
+
count,
|
|
66
|
+
createTableHandle,
|
|
67
|
+
sum,
|
|
68
|
+
type Aggregate,
|
|
69
|
+
type ColumnName,
|
|
70
|
+
type ColumnValue,
|
|
71
|
+
type InsertRow,
|
|
72
|
+
type Projected,
|
|
73
|
+
type Projection,
|
|
74
|
+
type RowOf,
|
|
75
|
+
type SelectQuery,
|
|
76
|
+
type TableHandle,
|
|
77
|
+
} from "./table/index.ts";
|
|
78
|
+
export {
|
|
79
|
+
openDatabase,
|
|
80
|
+
shapeReader,
|
|
81
|
+
wrapDatabase,
|
|
82
|
+
type Database,
|
|
83
|
+
type OpenConfig,
|
|
84
|
+
type QueryEvent,
|
|
85
|
+
type SqlitePragmaName,
|
|
86
|
+
} from "./database/index.ts";
|
|
87
|
+
export {
|
|
88
|
+
ensureSchema,
|
|
89
|
+
planSchema,
|
|
90
|
+
type EnsureSchemaOptions,
|
|
91
|
+
type SchemaChange,
|
|
92
|
+
} from "./migrate.ts";
|
|
93
|
+
export {
|
|
94
|
+
aliasedTable,
|
|
95
|
+
cteSql,
|
|
96
|
+
except,
|
|
97
|
+
forUpdate,
|
|
98
|
+
intersect,
|
|
99
|
+
union,
|
|
100
|
+
withCtes,
|
|
101
|
+
type Cte,
|
|
102
|
+
} from "./compound.ts";
|
|
103
|
+
export { indexCatalog, type CatalogIndex } from "./catalog.ts";
|
|
104
|
+
export { classifyError, DbError, type DbErrorKind } from "./error.ts";
|
|
105
|
+
export { dropColumnSql, dropIndexSql } from "./dialect.ts";
|
|
106
|
+
/** 表达式/条件:业务侧用 `E.eq(table.f_id, id)` 这样写。 */
|
|
107
|
+
export * as E from "./expr.ts";
|
|
108
|
+
export {
|
|
109
|
+
MigrationRegistry,
|
|
110
|
+
runMigrations,
|
|
111
|
+
schemaMigrations,
|
|
112
|
+
type Migration,
|
|
113
|
+
type RunMigrationsOptions,
|
|
114
|
+
} from "./migration.ts";
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addColumnSql,
|
|
3
|
+
createIndexSql,
|
|
4
|
+
createTableSql,
|
|
5
|
+
dropColumnSql,
|
|
6
|
+
type Dialect,
|
|
7
|
+
type TableShape,
|
|
8
|
+
} from "./dialect.ts";
|
|
9
|
+
import type { Catalog, ColumnDef } from "./defs.ts";
|
|
10
|
+
import type { Database } from "./database/types.ts";
|
|
11
|
+
import { shapeReader } from "./database/wrap.ts";
|
|
12
|
+
import { fragment } from "./fragment.ts";
|
|
13
|
+
|
|
14
|
+
/** 一条结构变更(按顺序执行)。 */
|
|
15
|
+
export interface SchemaChange {
|
|
16
|
+
readonly table: string;
|
|
17
|
+
readonly kind: "create-table" | "add-column" | "create-index" | "drop-column";
|
|
18
|
+
readonly statement: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EnsureSchemaOptions {
|
|
22
|
+
/** PostgreSQL 的 schema(默认 `public`);SQLite 忽略。 */
|
|
23
|
+
schema?: string;
|
|
24
|
+
/** 允许删掉定义里已经不存在、且确实多出来的列(默认关闭:升级不丢数据)。 */
|
|
25
|
+
allowDestructive?: boolean;
|
|
26
|
+
/** 需要看到实际执行的语句时(排查 / 测试)。 */
|
|
27
|
+
onStatement?: (statement: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 结构差异(纯函数):缺表建表、缺列补列、缺索引补索引。
|
|
32
|
+
*
|
|
33
|
+
* 默认只做「增」(缺表建表、缺列补列、缺索引补索引),不改类型、不删索引;删多出来的列只由
|
|
34
|
+
* `allowDestructive: true` 显式打开(旧库升级默认不丢数据)。
|
|
35
|
+
* @param dialect - 目标方言。
|
|
36
|
+
* @param catalog - 表定义(唯一事实来源)。
|
|
37
|
+
* @param shapes - 现有结构(表名 → 列 / 索引;不存在则 undefined)。
|
|
38
|
+
* @param options - schema 名。
|
|
39
|
+
* @returns 需要执行的变更,按依赖顺序(表 → 列 → 索引)。
|
|
40
|
+
*/
|
|
41
|
+
export function planSchema(
|
|
42
|
+
dialect: Dialect,
|
|
43
|
+
catalog: Catalog,
|
|
44
|
+
shapes: ReadonlyMap<string, TableShape | undefined>,
|
|
45
|
+
options: { schema?: string; allowDestructive?: boolean } = {},
|
|
46
|
+
): SchemaChange[] {
|
|
47
|
+
const changes: SchemaChange[] = [];
|
|
48
|
+
for (const table of catalog) {
|
|
49
|
+
const shape = shapes.get(table.name);
|
|
50
|
+
if (shape === undefined) {
|
|
51
|
+
changes.push({
|
|
52
|
+
table: table.name,
|
|
53
|
+
kind: "create-table",
|
|
54
|
+
statement: createTableSql(dialect, table, options.schema),
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
for (const [name, column] of Object.entries(table.columns)) {
|
|
58
|
+
if (shape.columns.has(name)) continue;
|
|
59
|
+
changes.push({
|
|
60
|
+
table: table.name,
|
|
61
|
+
kind: "add-column",
|
|
62
|
+
statement: addColumnSql(
|
|
63
|
+
dialect,
|
|
64
|
+
table.name,
|
|
65
|
+
name,
|
|
66
|
+
assertAddable(table.name, name, column),
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (options.allowDestructive === true) {
|
|
71
|
+
for (const name of shape.columns) {
|
|
72
|
+
if (table.columns[name] !== undefined) continue;
|
|
73
|
+
changes.push({
|
|
74
|
+
table: table.name,
|
|
75
|
+
kind: "drop-column",
|
|
76
|
+
statement: dropColumnSql(dialect, table.name, name),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const [name, index] of Object.entries(table.indexes ?? {})) {
|
|
82
|
+
if (shape?.indexes.has(name) === true) continue;
|
|
83
|
+
changes.push({
|
|
84
|
+
table: table.name,
|
|
85
|
+
kind: "create-index",
|
|
86
|
+
statement: createIndexSql(dialect, table, name, index.columns),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const [name, unique] of Object.entries(table.uniques ?? {})) {
|
|
90
|
+
if (shape?.indexes.has(name) === true) continue;
|
|
91
|
+
// 旧表上补唯一约束只能走唯一索引(不改表定义)。
|
|
92
|
+
changes.push({
|
|
93
|
+
table: table.name,
|
|
94
|
+
kind: "create-index",
|
|
95
|
+
statement: createIndexSql(dialect, table, name, unique.columns, true),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return changes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertAddable(table: string, name: string, column: ColumnDef): ColumnDef {
|
|
103
|
+
// 已有数据的表加 NOT NULL 列必须带默认值,否则方言会拒绝(或写入空值)。
|
|
104
|
+
if (column.notNull === true && column.default === undefined) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`rdbkit: cannot add NOT NULL column '${table}.${name}' without a default to an existing table`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return column;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 让库结构与表定义对齐,并返回实际执行的语句。 */
|
|
113
|
+
export async function ensureSchema(
|
|
114
|
+
database: Database,
|
|
115
|
+
catalog: Catalog,
|
|
116
|
+
options: EnsureSchemaOptions = {},
|
|
117
|
+
): Promise<string[]> {
|
|
118
|
+
const shapes = new Map<string, TableShape | undefined>();
|
|
119
|
+
for (const table of catalog) {
|
|
120
|
+
shapes.set(
|
|
121
|
+
table.name,
|
|
122
|
+
await database.dialect.readShape(shapeReader(database), table.name, options.schema),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const changes = planSchema(database.dialect, catalog, shapes, options);
|
|
126
|
+
for (const change of changes) {
|
|
127
|
+
options.onStatement?.(change.statement);
|
|
128
|
+
await database.run(fragment(change.statement));
|
|
129
|
+
}
|
|
130
|
+
return changes.map((change) => change.statement);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type { TableShape };
|