@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/migration.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Catalog, TableDef } from "./defs.ts";
|
|
2
|
+
import { defineTable } from "./defs.ts";
|
|
3
|
+
import type { Database } from "./database/types.ts";
|
|
4
|
+
import { ensureSchema } from "./migrate.ts";
|
|
5
|
+
|
|
6
|
+
/** 内置迁移账本:一条已执行的数据迁移一行(`f_id` 唯一)。 */
|
|
7
|
+
export const schemaMigrations = defineTable({
|
|
8
|
+
name: "t_schema_migrations",
|
|
9
|
+
columns: {
|
|
10
|
+
f_id: { type: "text", primaryKey: true, notNull: true },
|
|
11
|
+
f_applied_at: { type: "bigint", notNull: true },
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** 一条数据迁移:`id` 唯一且按字典序执行;`up` 在事务里跑,抛错则整条不记。 */
|
|
16
|
+
export interface Migration {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/**
|
|
19
|
+
* 返回 `false` 表示这次无需执行(例如旧表还不存在),**不记账**——下次打开还会再试。
|
|
20
|
+
* 返回其它值(含 `undefined`)表示已执行,记账。
|
|
21
|
+
*/
|
|
22
|
+
readonly up: (database: Database) => Promise<boolean | void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 迁移注册表:业务把自己的迁移登记进来,打开库时一起执行。 */
|
|
26
|
+
export class MigrationRegistry {
|
|
27
|
+
readonly #migrations = new Map<string, Migration>();
|
|
28
|
+
|
|
29
|
+
register(migration: Migration): this {
|
|
30
|
+
if (this.#migrations.has(migration.id)) {
|
|
31
|
+
throw new Error(`rdbkit: migration '${migration.id}' is registered twice`);
|
|
32
|
+
}
|
|
33
|
+
this.#migrations.set(migration.id, migration);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
list(): readonly Migration[] {
|
|
38
|
+
return [...this.#migrations.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 打开库时做的事:结构(catalog diff)+ 数据迁移(注册表)。 */
|
|
43
|
+
export interface RunMigrationsOptions {
|
|
44
|
+
/** 业务表定义(`ensureSchema` 用)。 */
|
|
45
|
+
catalog?: Catalog;
|
|
46
|
+
migrations: readonly Migration[] | MigrationRegistry;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 先按 catalog 对齐结构(建表 / 补列 / 补索引),再执行未应用的数据迁移。
|
|
51
|
+
*
|
|
52
|
+
* 账本是 rdbkit 内置的 `t_schema_migrations`:每条迁移在**同一个事务**里执行并记账,
|
|
53
|
+
* 失败即回滚,重跑时只跑没记过的。返回本次实际执行了哪些 `id`。
|
|
54
|
+
*/
|
|
55
|
+
export async function runMigrations(
|
|
56
|
+
database: Database,
|
|
57
|
+
options: RunMigrationsOptions,
|
|
58
|
+
): Promise<readonly string[]> {
|
|
59
|
+
const migrations =
|
|
60
|
+
options.migrations instanceof MigrationRegistry
|
|
61
|
+
? options.migrations.list()
|
|
62
|
+
: [...options.migrations];
|
|
63
|
+
const catalog: Catalog = [...(options.catalog ?? []), schemaMigrations as TableDef];
|
|
64
|
+
await ensureSchema(database, catalog);
|
|
65
|
+
|
|
66
|
+
const ledger = database.table(schemaMigrations);
|
|
67
|
+
const applied = new Set<string>();
|
|
68
|
+
for await (const row of ledger.select({ id: ledger.f_id })) applied.add(row.id);
|
|
69
|
+
|
|
70
|
+
const executed: string[] = [];
|
|
71
|
+
for (const migration of migrations) {
|
|
72
|
+
if (applied.has(migration.id)) continue;
|
|
73
|
+
const appliedNow = await database.transaction(async (tx) => {
|
|
74
|
+
const result = await migration.up(tx);
|
|
75
|
+
if (result === false) return false;
|
|
76
|
+
await tx.table(schemaMigrations).insert({ f_id: migration.id, f_applied_at: Date.now() });
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
if (appliedNow) executed.push(migration.id);
|
|
80
|
+
}
|
|
81
|
+
return executed;
|
|
82
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { TableDef } from "../defs.ts";
|
|
2
|
+
import type { ColumnRef, Condition, Operand } from "../expr.ts";
|
|
3
|
+
import type { Fragment } from "../fragment.ts";
|
|
4
|
+
import type { Database } from "../database/types.ts";
|
|
5
|
+
import {
|
|
6
|
+
type ColumnName,
|
|
7
|
+
type InsertRow,
|
|
8
|
+
type Projected,
|
|
9
|
+
type Projection,
|
|
10
|
+
type RowOf,
|
|
11
|
+
type UpdatePatch,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
import { selectQuery, type SelectQuery } from "./select.ts";
|
|
14
|
+
import {
|
|
15
|
+
deleteOf,
|
|
16
|
+
insertFromSelectOf,
|
|
17
|
+
insertManyOf,
|
|
18
|
+
insertOf,
|
|
19
|
+
updateOf,
|
|
20
|
+
upsertExprOf,
|
|
21
|
+
} from "./render.ts";
|
|
22
|
+
|
|
23
|
+
/** 列访问器:`t.f_domain` 就是这一列的引用(带值类型)。 */
|
|
24
|
+
export type ColumnAccessors<Row> = { readonly [K in ColumnName<Row>]: ColumnRef<Row[K]> };
|
|
25
|
+
|
|
26
|
+
/** 一张表的业务面:投影查询 + 写入,调用方不接触方言与 SQL。 */
|
|
27
|
+
export type TableHandle<T extends TableDef> = TableHandleBase<T> & ColumnAccessors<RowOf<T>>;
|
|
28
|
+
|
|
29
|
+
export interface TableHandleBase<T extends TableDef> {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly definition: T;
|
|
32
|
+
/** 拿一列(带值类型的引用),投影 / 条件里用。 */
|
|
33
|
+
column<K extends ColumnName<RowOf<T>>>(name: K): ColumnRef<RowOf<T>[K]>;
|
|
34
|
+
/** 投影省略时取全部列(结果按列名原样)。 */
|
|
35
|
+
select<P extends Projection = Record<string, ColumnRef>>(
|
|
36
|
+
projection?: P,
|
|
37
|
+
): SelectQuery<Projected<P>>;
|
|
38
|
+
/** 插入;传 `returning` 时拿回这些列(`RETURNING`,两方言都支持)。 */
|
|
39
|
+
insert(
|
|
40
|
+
row: InsertRow<RowOf<T>>,
|
|
41
|
+
returning?: readonly ColumnName<RowOf<T>>[],
|
|
42
|
+
): Promise<RowOf<T>[]>;
|
|
43
|
+
insertIgnore(row: InsertRow<RowOf<T>>, conflict: readonly ColumnName<RowOf<T>>[]): Promise<void>;
|
|
44
|
+
/** `INSERT … SELECT`:把一条查询的结果按列顺序插进来(Go 的 `InsertFrom`)。 */
|
|
45
|
+
insertFromSelect(
|
|
46
|
+
columns: readonly ColumnName<RowOf<T>>[],
|
|
47
|
+
source: Fragment,
|
|
48
|
+
options?: {
|
|
49
|
+
conflict?: readonly ColumnName<RowOf<T>>[];
|
|
50
|
+
update?: readonly ColumnName<RowOf<T>>[];
|
|
51
|
+
},
|
|
52
|
+
): Promise<void>;
|
|
53
|
+
/** 批量插入;传 `conflict` / `update` 时是批量 upsert(`ON CONFLICT … DO UPDATE`)。 */
|
|
54
|
+
insertMany(
|
|
55
|
+
rows: readonly InsertRow<RowOf<T>>[],
|
|
56
|
+
options?: {
|
|
57
|
+
conflict?: readonly ColumnName<RowOf<T>>[];
|
|
58
|
+
update?: readonly ColumnName<RowOf<T>>[];
|
|
59
|
+
},
|
|
60
|
+
): Promise<void>;
|
|
61
|
+
upsert(
|
|
62
|
+
row: InsertRow<RowOf<T>>,
|
|
63
|
+
conflict: readonly ColumnName<RowOf<T>>[],
|
|
64
|
+
update?: readonly ColumnName<RowOf<T>>[],
|
|
65
|
+
): Promise<void>;
|
|
66
|
+
update(
|
|
67
|
+
patch: UpdatePatch<RowOf<T>>,
|
|
68
|
+
condition?: Condition,
|
|
69
|
+
returning?: readonly ColumnName<RowOf<T>>[],
|
|
70
|
+
): Promise<RowOf<T>[]>;
|
|
71
|
+
/** `ON CONFLICT ... DO UPDATE SET 列 = 表达式`(如自增计数)。 */
|
|
72
|
+
upsertExpr(
|
|
73
|
+
row: InsertRow<RowOf<T>>,
|
|
74
|
+
conflict: readonly ColumnName<RowOf<T>>[],
|
|
75
|
+
assignments: Record<string, Operand>,
|
|
76
|
+
): Promise<void>;
|
|
77
|
+
/** 删除;传 `returning` 时拿回被删行的这些列(`RETURNING`)。 */
|
|
78
|
+
delete(condition?: Condition, returning?: readonly ColumnName<RowOf<T>>[]): Promise<RowOf<T>[]>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createTableHandle<T extends TableDef>(
|
|
82
|
+
database: Database,
|
|
83
|
+
definition: T,
|
|
84
|
+
): TableHandle<T> {
|
|
85
|
+
const name = definition.name;
|
|
86
|
+
const handle: TableHandleBase<T> = {
|
|
87
|
+
name,
|
|
88
|
+
definition,
|
|
89
|
+
column: (column) => ({ kind: "column", table: name, name: column }) as never,
|
|
90
|
+
select: (projection) =>
|
|
91
|
+
selectQuery(
|
|
92
|
+
database,
|
|
93
|
+
definition,
|
|
94
|
+
projection ??
|
|
95
|
+
(Object.fromEntries(
|
|
96
|
+
Object.keys(definition.columns).map((name) => [
|
|
97
|
+
name,
|
|
98
|
+
{ kind: "column", table: definition.name, name },
|
|
99
|
+
]),
|
|
100
|
+
) as Projection),
|
|
101
|
+
) as never,
|
|
102
|
+
async insert(row, returning) {
|
|
103
|
+
return await database.write<RowOf<T>>(
|
|
104
|
+
insertOf(definition, row, "insert", undefined, undefined, returning),
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
async insertIgnore(row, conflict) {
|
|
108
|
+
await database.run(insertOf(definition, row, "ignore", conflict));
|
|
109
|
+
},
|
|
110
|
+
async insertFromSelect(columns, source, options) {
|
|
111
|
+
await database.run(insertFromSelectOf(definition, columns, source, options));
|
|
112
|
+
},
|
|
113
|
+
async insertMany(rows, options) {
|
|
114
|
+
await database.run(insertManyOf(definition, rows, options));
|
|
115
|
+
},
|
|
116
|
+
async upsert(row, conflict, update) {
|
|
117
|
+
await database.run(insertOf(definition, row, "upsert", conflict, update));
|
|
118
|
+
},
|
|
119
|
+
async update(patch, condition, returning) {
|
|
120
|
+
return await database.write<RowOf<T>>(updateOf(definition, patch, condition, returning));
|
|
121
|
+
},
|
|
122
|
+
async upsertExpr(row, conflict, assignments) {
|
|
123
|
+
await database.run(upsertExprOf(definition, row, conflict, assignments));
|
|
124
|
+
},
|
|
125
|
+
async delete(condition, returning) {
|
|
126
|
+
if (returning !== undefined && returning.length > 0) {
|
|
127
|
+
return await database.write<RowOf<T>>(deleteOf(definition, condition, returning));
|
|
128
|
+
}
|
|
129
|
+
await database.run(deleteOf(definition, condition));
|
|
130
|
+
return [];
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
// 列名直接当属性读:`t.f_domain` 等价于 `t.column("f_domain")`。
|
|
134
|
+
return new Proxy(handle, {
|
|
135
|
+
get(target, property, receiver) {
|
|
136
|
+
if (typeof property === "string" && definition.columns[property] !== undefined) {
|
|
137
|
+
return { kind: "column", table: name, name: property } satisfies ColumnRef;
|
|
138
|
+
}
|
|
139
|
+
return Reflect.get(target, property, receiver) as unknown;
|
|
140
|
+
},
|
|
141
|
+
}) as TableHandle<T>;
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createTableHandle, type ColumnAccessors, type TableHandle, type TableHandleBase } from "./handle.ts";
|
|
2
|
+
export { count, selectQuery, sum, type GroupByItem, type SelectQuery } from "./select.ts";
|
|
3
|
+
export {
|
|
4
|
+
type Aggregate,
|
|
5
|
+
type ColumnName,
|
|
6
|
+
type ColumnValue,
|
|
7
|
+
type InsertRow,
|
|
8
|
+
type Projected,
|
|
9
|
+
type Projection,
|
|
10
|
+
type RowOf,
|
|
11
|
+
type UpdatePatch,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
export { columnSql } from "../expr.ts";
|
|
14
|
+
export { render } from "../fragment.ts";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { TableDef } from "../defs.ts";
|
|
2
|
+
import { concat, fragment, param, text, type Fragment } from "../fragment.ts";
|
|
3
|
+
import { operandSql, type Condition, type Operand } from "../expr.ts";
|
|
4
|
+
|
|
5
|
+
export function returningClause(returning?: readonly string[]): Fragment | string {
|
|
6
|
+
return returning === undefined || returning.length === 0
|
|
7
|
+
? ""
|
|
8
|
+
: text(`RETURNING ${returning.map((name) => `"${name}"`).join(", ")}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function insertOf(
|
|
12
|
+
table: TableDef,
|
|
13
|
+
row: Record<string, unknown>,
|
|
14
|
+
mode: "insert" | "ignore" | "upsert",
|
|
15
|
+
conflict?: readonly string[],
|
|
16
|
+
update?: readonly string[],
|
|
17
|
+
returning?: readonly string[],
|
|
18
|
+
): Fragment {
|
|
19
|
+
const columns = Object.keys(row).filter((name) => table.columns[name] !== undefined);
|
|
20
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
21
|
+
const sql =
|
|
22
|
+
`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) ` +
|
|
23
|
+
`VALUES (${columns.map(() => "?").join(", ")})`;
|
|
24
|
+
const base = fragment(
|
|
25
|
+
sql,
|
|
26
|
+
columns.map((name) => row[name]),
|
|
27
|
+
);
|
|
28
|
+
if (mode === "insert") return concat([base, returningClause(returning)], " ");
|
|
29
|
+
const target = (conflict ?? []).map((name) => `"${name}"`).join(", ");
|
|
30
|
+
if (mode === "ignore") return concat([base, `ON CONFLICT (${target}) DO NOTHING`], " ");
|
|
31
|
+
const updates =
|
|
32
|
+
(update ?? []).length === 0
|
|
33
|
+
? "DO NOTHING"
|
|
34
|
+
: `DO UPDATE SET ${(update ?? [])
|
|
35
|
+
.map((name) => `"${name}" = excluded."${name}"`)
|
|
36
|
+
.join(", ")}`;
|
|
37
|
+
return concat([base, `ON CONFLICT (${target}) ${updates}`, returningClause(returning)], " ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function insertManyOf(
|
|
41
|
+
table: TableDef,
|
|
42
|
+
rows: readonly Record<string, unknown>[],
|
|
43
|
+
options: { conflict?: readonly string[]; update?: readonly string[] } = {},
|
|
44
|
+
): Fragment {
|
|
45
|
+
if (rows.length === 0) return fragment("SELECT 1 WHERE 1 = 0");
|
|
46
|
+
const columns = Object.keys(rows[0]!).filter((name) => table.columns[name] !== undefined);
|
|
47
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
for (const column of columns) {
|
|
50
|
+
if (row[column] === undefined) {
|
|
51
|
+
throw new Error(`rdbkit: insertMany into "${table.name}" is missing column '${column}'`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const tuple = `(${columns.map(() => "?").join(", ")})`;
|
|
56
|
+
const sql =
|
|
57
|
+
`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) ` +
|
|
58
|
+
`VALUES ${rows.map(() => tuple).join(", ")}`;
|
|
59
|
+
const insert = fragment(
|
|
60
|
+
sql,
|
|
61
|
+
rows.flatMap((row) => columns.map((name) => row[name])),
|
|
62
|
+
);
|
|
63
|
+
return concat([insert, conflictClause(options)], " ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function insertFromSelectOf(
|
|
67
|
+
table: TableDef,
|
|
68
|
+
columns: readonly string[],
|
|
69
|
+
source: Fragment,
|
|
70
|
+
options: { conflict?: readonly string[]; update?: readonly string[] } = {},
|
|
71
|
+
): Fragment {
|
|
72
|
+
if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
|
|
73
|
+
const head = text(
|
|
74
|
+
`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")})`,
|
|
75
|
+
);
|
|
76
|
+
return concat([head, source, conflictClause(options)], " ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `ON CONFLICT …` 子句(没有冲突列时为空)。 */
|
|
80
|
+
export function conflictClause(options: {
|
|
81
|
+
conflict?: readonly string[];
|
|
82
|
+
update?: readonly string[];
|
|
83
|
+
}): Fragment | string {
|
|
84
|
+
const conflict = options.conflict ?? [];
|
|
85
|
+
if (conflict.length === 0) return "";
|
|
86
|
+
const target = conflict.map((name) => `"${name}"`).join(", ");
|
|
87
|
+
const updates =
|
|
88
|
+
(options.update ?? []).length === 0
|
|
89
|
+
? "DO NOTHING"
|
|
90
|
+
: `DO UPDATE SET ${(options.update ?? [])
|
|
91
|
+
.map((name) => `"${name}" = excluded."${name}"`)
|
|
92
|
+
.join(", ")}`;
|
|
93
|
+
return text(`ON CONFLICT (${target}) ${updates}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function upsertExprOf(
|
|
97
|
+
table: TableDef,
|
|
98
|
+
row: Record<string, unknown>,
|
|
99
|
+
conflict: readonly string[],
|
|
100
|
+
assignments: Record<string, Operand>,
|
|
101
|
+
): Fragment {
|
|
102
|
+
const base = insertOf(table, row, "insert");
|
|
103
|
+
const target = conflict.map((name) => `"${name}"`).join(", ");
|
|
104
|
+
const sets = Object.entries(assignments).map(([name, ref]) =>
|
|
105
|
+
concat([text(`"${name}" =`), operandSql(ref)], " "),
|
|
106
|
+
);
|
|
107
|
+
return concat([base, text(`ON CONFLICT (${target}) DO UPDATE SET`), concat(sets, ", ")], " ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function updateOf(
|
|
111
|
+
table: TableDef,
|
|
112
|
+
patch: Record<string, unknown>,
|
|
113
|
+
condition?: Condition,
|
|
114
|
+
returning?: readonly string[],
|
|
115
|
+
): Fragment {
|
|
116
|
+
const columns = Object.keys(patch).filter((name) => table.columns[name] !== undefined);
|
|
117
|
+
if (columns.length === 0) throw new Error(`rdbkit: update "${table.name}" has no columns`);
|
|
118
|
+
// 值可以是表达式(列自增、取大等):只有普通值才绑参数。
|
|
119
|
+
const sets = columns.map((name) => {
|
|
120
|
+
const value = patch[name];
|
|
121
|
+
const assigned =
|
|
122
|
+
typeof value === "object" && value !== null && "kind" in value
|
|
123
|
+
? operandSql(value as Operand)
|
|
124
|
+
: param(value);
|
|
125
|
+
return concat([text(`"${name}" =`), assigned], " ");
|
|
126
|
+
});
|
|
127
|
+
const head = concat([text(`UPDATE "${table.name}" SET`), concat(sets, ", ")], " ");
|
|
128
|
+
const body = condition === undefined ? head : concat([head, text("WHERE"), condition], " ");
|
|
129
|
+
return concat([body, returningClause(returning)], " ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function deleteOf(
|
|
133
|
+
table: TableDef,
|
|
134
|
+
condition?: Condition,
|
|
135
|
+
returning?: readonly string[],
|
|
136
|
+
): Fragment {
|
|
137
|
+
const head = text(`DELETE FROM "${table.name}"`);
|
|
138
|
+
const body = condition === undefined ? head : concat([head, text("WHERE"), condition], " ");
|
|
139
|
+
return concat([body, returningClause(returning)], " ");
|
|
140
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { TableDef } from "../defs.ts";
|
|
2
|
+
import { concat, fragment, only, text, type Fragment } from "../fragment.ts";
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
columnSql,
|
|
6
|
+
type ColumnRef,
|
|
7
|
+
type Condition,
|
|
8
|
+
type ExprOperand,
|
|
9
|
+
} from "../expr.ts";
|
|
10
|
+
import { forUpdate as forUpdateSql, withCtes, type Cte } from "../compound.ts";
|
|
11
|
+
import type { Database } from "../database/types.ts";
|
|
12
|
+
import type { Aggregate, Projection } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/** 表达式的片段:方言相关的表达式自带片段,普通的按 `sql` + 绑定参数编。 */
|
|
15
|
+
function exprFragment(expression: ExprOperand): Fragment {
|
|
16
|
+
return expression.fragment ?? fragment(expression.sql, expression.params);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** COUNT(*),投影里写 `{ n: count() }`。 */
|
|
20
|
+
export function count(): Aggregate<number | bigint> {
|
|
21
|
+
return { kind: "expr", sql: "COUNT(*)", params: [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SUM(列);多表查询时传表名避免歧义。
|
|
26
|
+
*
|
|
27
|
+
* PostgreSQL 的 `SUM(bigint)` 返回 `numeric`(驱动给字符串),SQLite 返回整数——统一 cast 回 int8,
|
|
28
|
+
* 声明的 `number | bigint` 才成立。列类型只有 integer / bigint,所以这个 cast 不丢小数。
|
|
29
|
+
*/
|
|
30
|
+
export function sum(name: string, table?: string): Aggregate<number | bigint> {
|
|
31
|
+
const target = table === undefined ? `"${name}"` : `"${table}"."${name}"`;
|
|
32
|
+
return {
|
|
33
|
+
kind: "expr",
|
|
34
|
+
sql: "",
|
|
35
|
+
params: [],
|
|
36
|
+
fragment: concat(
|
|
37
|
+
[
|
|
38
|
+
only("sqlite", text(`SUM(${target})`)),
|
|
39
|
+
only("postgres", text(`SUM(${target})::bigint`)),
|
|
40
|
+
],
|
|
41
|
+
"",
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 一个查询:实现 `AsyncIterable`,`for await` 直接消费(对应 Golang 的 `iter.Seq2`)。 */
|
|
47
|
+
export interface SelectQuery<T> extends AsyncIterable<T> {
|
|
48
|
+
where(condition: Condition): this;
|
|
49
|
+
/** `cross` 不需要 ON;`alias` 给被 join 的表起别名(投影里用 `column(名字, 别名)`)。 */
|
|
50
|
+
join(
|
|
51
|
+
table: TableDef,
|
|
52
|
+
on?: Condition,
|
|
53
|
+
kind?: "inner" | "left" | "right" | "full" | "cross",
|
|
54
|
+
alias?: string,
|
|
55
|
+
): this;
|
|
56
|
+
/** `WITH "name" AS (<query>)`,多条按顺序拼。 */
|
|
57
|
+
with(
|
|
58
|
+
name: string,
|
|
59
|
+
query: Fragment,
|
|
60
|
+
options?: { recursive?: boolean; columns?: readonly string[] },
|
|
61
|
+
): this;
|
|
62
|
+
/** 分组:列名(主表)、带表名的列引用或表达式。 */
|
|
63
|
+
groupBy(...columns: GroupByItem[]): this;
|
|
64
|
+
having(condition: Condition): this;
|
|
65
|
+
distinct(): this;
|
|
66
|
+
orderBy(column: string | ColumnRef | ExprOperand, direction?: "asc" | "desc"): this;
|
|
67
|
+
limit(count: number): this;
|
|
68
|
+
offset(count: number): this;
|
|
69
|
+
/** 行锁(PostgreSQL 生效,SQLite 渲染时丢弃)。 */
|
|
70
|
+
forUpdate(options?: { of?: readonly string[]; noWait?: boolean; skipLocked?: boolean }): this;
|
|
71
|
+
toFragment(): Fragment;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `groupBy` 的入参:列名、带表名的列引用或表达式。 */
|
|
75
|
+
export type GroupByItem = string | ColumnRef | ExprOperand;
|
|
76
|
+
|
|
77
|
+
export function selectQuery<T>(
|
|
78
|
+
database: Database,
|
|
79
|
+
table: TableDef,
|
|
80
|
+
projection: Projection,
|
|
81
|
+
): SelectQuery<T> {
|
|
82
|
+
const state: {
|
|
83
|
+
condition?: Condition | undefined;
|
|
84
|
+
joins: {
|
|
85
|
+
table: TableDef;
|
|
86
|
+
on?: Condition | undefined;
|
|
87
|
+
kind: "inner" | "left" | "right" | "full" | "cross";
|
|
88
|
+
alias?: string | undefined;
|
|
89
|
+
}[];
|
|
90
|
+
ctes: Cte[];
|
|
91
|
+
lock?: Fragment | undefined;
|
|
92
|
+
having?: Condition | undefined;
|
|
93
|
+
distinct: boolean;
|
|
94
|
+
order: { column: string | ColumnRef | ExprOperand; direction: "asc" | "desc" }[];
|
|
95
|
+
group: GroupByItem[];
|
|
96
|
+
limit?: number | undefined;
|
|
97
|
+
offset?: number | undefined;
|
|
98
|
+
} = { joins: [], ctes: [], distinct: false, order: [], group: [] };
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 结果行的值归一:`bigint` 列统一成 JS `bigint`。
|
|
102
|
+
* SQLite 的 INTEGER 由驱动给 number,PostgreSQL 的 int8 已经是 BigInt(见 `bigintTypes`)——
|
|
103
|
+
* 读出来的类型不能跟着驱动走,否则同一份表定义在两种方言下值类型不同。
|
|
104
|
+
*/
|
|
105
|
+
const normalize = (row: Record<string, unknown>): T => {
|
|
106
|
+
for (const [alias, entry] of Object.entries(projection)) {
|
|
107
|
+
if (entry.kind !== "column") continue;
|
|
108
|
+
const owner = entry.table === undefined ? table : tableOf(entry.table);
|
|
109
|
+
if (owner?.columns[entry.name]?.type !== "bigint") continue;
|
|
110
|
+
const value = row[alias];
|
|
111
|
+
if (value === null || value === undefined || typeof value === "bigint") continue;
|
|
112
|
+
row[alias] = BigInt(value as number | string);
|
|
113
|
+
}
|
|
114
|
+
return row as T;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const tableOf = (name: string): TableDef | undefined => {
|
|
118
|
+
if (name === table.name) return table;
|
|
119
|
+
for (const join of state.joins) {
|
|
120
|
+
if ((join.alias ?? join.table.name) === name) return join.table;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const toFragment = (): Fragment => {
|
|
126
|
+
const items = Object.entries(projection).map(([alias, entry]) =>
|
|
127
|
+
entry.kind === "column"
|
|
128
|
+
? text(
|
|
129
|
+
`${entry.table === undefined ? "" : `"${entry.table}".`}"${entry.name}" AS "${alias}"`,
|
|
130
|
+
)
|
|
131
|
+
: concat([exprFragment(entry), text(`AS "${alias}"`)], " "),
|
|
132
|
+
);
|
|
133
|
+
const parts: (Fragment | string)[] = [
|
|
134
|
+
state.distinct ? text("SELECT DISTINCT") : text("SELECT"),
|
|
135
|
+
concat(items, ", "),
|
|
136
|
+
text(`FROM "${table.name}"`),
|
|
137
|
+
];
|
|
138
|
+
for (const join of state.joins) {
|
|
139
|
+
const target =
|
|
140
|
+
join.alias === undefined
|
|
141
|
+
? `"${join.table.name}"`
|
|
142
|
+
: `"${join.table.name}" AS "${join.alias}"`;
|
|
143
|
+
if (join.kind === "cross") {
|
|
144
|
+
parts.push(text(`CROSS JOIN ${target}`));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const keyword = join.kind === "inner" ? "JOIN" : `${join.kind.toUpperCase()} JOIN`;
|
|
148
|
+
parts.push(text(`${keyword} ${target} ON`), join.on ?? text("1 = 1"));
|
|
149
|
+
}
|
|
150
|
+
if (state.condition !== undefined) parts.push(text("WHERE"), state.condition);
|
|
151
|
+
if (state.group.length > 0) {
|
|
152
|
+
const groupParts = state.group.map((item) =>
|
|
153
|
+
typeof item === "string"
|
|
154
|
+
? text(`"${table.name}"."${item}"`)
|
|
155
|
+
: item.kind === "column"
|
|
156
|
+
? text(columnSql(item))
|
|
157
|
+
: exprFragment(item),
|
|
158
|
+
);
|
|
159
|
+
parts.push(text("GROUP BY"), concat(groupParts, ", "));
|
|
160
|
+
}
|
|
161
|
+
if (state.having !== undefined) parts.push(text("HAVING"), state.having);
|
|
162
|
+
if (state.order.length > 0) {
|
|
163
|
+
const orderParts = state.order.map((order) => {
|
|
164
|
+
const ref =
|
|
165
|
+
typeof order.column === "string"
|
|
166
|
+
? text(`"${table.name}"."${order.column}"`)
|
|
167
|
+
: order.column.kind === "column"
|
|
168
|
+
? text(columnSql(order.column))
|
|
169
|
+
: exprFragment(order.column);
|
|
170
|
+
return concat([ref, text(order.direction.toUpperCase())], " ");
|
|
171
|
+
});
|
|
172
|
+
parts.push(text("ORDER BY"), concat(orderParts, ", "));
|
|
173
|
+
}
|
|
174
|
+
if (state.limit !== undefined) parts.push(fragment("LIMIT ?", [state.limit]));
|
|
175
|
+
if (state.offset !== undefined) parts.push(fragment("OFFSET ?", [state.offset]));
|
|
176
|
+
if (state.lock !== undefined) parts.push(state.lock);
|
|
177
|
+
return withCtes(state.ctes, concat(parts, " "));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const query: SelectQuery<T> = {
|
|
181
|
+
where(condition) {
|
|
182
|
+
state.condition = state.condition === undefined ? condition : and(state.condition, condition);
|
|
183
|
+
return query;
|
|
184
|
+
},
|
|
185
|
+
join(joined, on, kind = "inner", alias) {
|
|
186
|
+
state.joins.push({ table: joined, on, kind, alias });
|
|
187
|
+
return query;
|
|
188
|
+
},
|
|
189
|
+
with(name, queryText, options = {}) {
|
|
190
|
+
state.ctes.push({ name, query: queryText, ...options });
|
|
191
|
+
return query;
|
|
192
|
+
},
|
|
193
|
+
forUpdate(options) {
|
|
194
|
+
state.lock = forUpdateSql(options);
|
|
195
|
+
return query;
|
|
196
|
+
},
|
|
197
|
+
groupBy(...columns) {
|
|
198
|
+
state.group.push(...columns);
|
|
199
|
+
return query;
|
|
200
|
+
},
|
|
201
|
+
having(condition) {
|
|
202
|
+
state.having = condition;
|
|
203
|
+
return query;
|
|
204
|
+
},
|
|
205
|
+
distinct() {
|
|
206
|
+
state.distinct = true;
|
|
207
|
+
return query;
|
|
208
|
+
},
|
|
209
|
+
orderBy(column, direction = "asc") {
|
|
210
|
+
state.order.push({ column, direction });
|
|
211
|
+
return query;
|
|
212
|
+
},
|
|
213
|
+
limit(count) {
|
|
214
|
+
state.limit = count;
|
|
215
|
+
return query;
|
|
216
|
+
},
|
|
217
|
+
offset(count) {
|
|
218
|
+
state.offset = count;
|
|
219
|
+
return query;
|
|
220
|
+
},
|
|
221
|
+
toFragment,
|
|
222
|
+
async *[Symbol.asyncIterator]() {
|
|
223
|
+
for await (const row of database.iterate<Record<string, unknown>>(toFragment())) {
|
|
224
|
+
yield normalize(row);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
return query;
|
|
229
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ColumnDef, TableDef } from "../defs.ts";
|
|
2
|
+
import type { ColumnRef, ExprOperand, Operand } from "../expr.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 一列对外的 TS 值类型:文本 → string、布尔 → boolean、64 位整数(`bigint`)→ JS `bigint`(精确,
|
|
6
|
+
* 对应 Go 的 int64 / uint64;不能用 number,超过 `2^53` 会丢精度)、其余 → number。
|
|
7
|
+
* 非 NOT NULL 允许 null。
|
|
8
|
+
*/
|
|
9
|
+
export type ColumnValue<C extends ColumnDef> = C["type"] extends "text"
|
|
10
|
+
? string
|
|
11
|
+
: C["type"] extends "boolean"
|
|
12
|
+
? boolean
|
|
13
|
+
: C["type"] extends "bigint"
|
|
14
|
+
? bigint
|
|
15
|
+
: number;
|
|
16
|
+
|
|
17
|
+
export type RowOf<T extends TableDef> = {
|
|
18
|
+
[K in keyof T["columns"]]: T["columns"][K] extends { notNull: true }
|
|
19
|
+
? ColumnValue<T["columns"][K]>
|
|
20
|
+
: T["columns"][K] extends { type: "serial" }
|
|
21
|
+
? ColumnValue<T["columns"][K]>
|
|
22
|
+
: ColumnValue<T["columns"][K]> | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 写入时 `bigint` 列也接受 number(安全整数范围内两者等价)。 */
|
|
26
|
+
type Writable<T> = T extends bigint ? bigint | number : T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 写入行:列可选(有默认值 / 自增列不必给)。值允许 number 写进 `bigint` 列,业务不必到处写 `n` 后缀;
|
|
30
|
+
* 读回来仍然是 `bigint`(不丢精度)。
|
|
31
|
+
*/
|
|
32
|
+
export type InsertRow<Row> = Partial<{ [K in keyof Row]: Writable<Row[K]> }>;
|
|
33
|
+
|
|
34
|
+
/** 更新时的值:普通值(绑定参数)或表达式(`expr('"f_revision" + 1')` 等)。 */
|
|
35
|
+
export type UpdatePatch<Row> = { [K in keyof Row]?: Writable<Row[K]> | Operand };
|
|
36
|
+
|
|
37
|
+
/** 列名(字符串键)。 */
|
|
38
|
+
export type ColumnName<Row> = Extract<keyof Row, string>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 聚合表达式(`count()` / `sum(col)`):投影与条件里都能用。
|
|
42
|
+
*
|
|
43
|
+
* 值是 64 位整数:PostgreSQL 给 `bigint`、SQLite 给 number,业务要 number 时显式 `Number(x)`。
|
|
44
|
+
*/
|
|
45
|
+
export type Aggregate<T = number | bigint> = ExprOperand<T>;
|
|
46
|
+
|
|
47
|
+
/** 投影:别名 → 列引用或聚合。结果类型由它推导。 */
|
|
48
|
+
export type Projection = Record<string, ColumnRef<any> | Aggregate<any>>;
|
|
49
|
+
|
|
50
|
+
/** 投影的强类型结果。 */
|
|
51
|
+
export type Projected<P extends Projection> = {
|
|
52
|
+
[K in keyof P]: P[K] extends ColumnRef<infer T> ? T : P[K] extends Aggregate<infer T> ? T : never;
|
|
53
|
+
};
|