@morlay/rdbkit 0.0.0-stage → 0.0.1-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/dist/index.d.mts +518 -0
- package/dist/index.mjs +1422 -0
- package/dist/testing.d.mts +28 -0
- package/dist/testing.mjs +42 -0
- package/package.json +49 -3
- package/src/__tests__/database-introspect.spec.ts +71 -0
- package/src/__tests__/expr-functions.spec.ts +64 -0
- package/src/__tests__/migration.spec.ts +81 -0
- package/src/__tests__/pg.spec.ts +123 -0
- package/src/__tests__/query-join.spec.ts +114 -0
- package/src/__tests__/query-p1.spec.ts +108 -0
- package/src/__tests__/query-write.spec.ts +293 -0
- package/src/__tests__/query.spec.ts +88 -0
- package/src/__tests__/schema.spec.ts +144 -0
- package/src/__tests__/transaction.spec.ts +83 -0
- package/src/catalog.ts +24 -0
- package/src/compound.ts +64 -0
- package/src/database/capabilities.ts +70 -0
- package/src/database/create.ts +119 -0
- package/src/database/index.ts +9 -0
- package/src/database/postgres.ts +133 -0
- package/src/database/queue.ts +24 -0
- package/src/database/sqlite.ts +66 -0
- package/src/database/types.ts +87 -0
- package/src/database/wrap.ts +48 -0
- package/src/defs.ts +82 -0
- package/src/dialect.ts +219 -0
- package/src/error.ts +72 -0
- package/src/expr.ts +346 -0
- package/src/fragment.ts +96 -0
- package/src/index.ts +114 -0
- package/src/migrate.ts +133 -0
- package/src/migration.ts +82 -0
- package/src/table/handle.ts +142 -0
- package/src/table/index.ts +14 -0
- package/src/table/render.ts +140 -0
- package/src/table/select.ts +229 -0
- package/src/table/types.ts +53 -0
- package/src/testing.ts +56 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createDialect, type DialectName } from "../dialect.ts";
|
|
2
|
+
import { fragment } from "../fragment.ts";
|
|
3
|
+
import { indexCatalog } from "../catalog.ts";
|
|
4
|
+
import { createDatabase } from "./create.ts";
|
|
5
|
+
import type { Database } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** 把「已有执行面」包成 rdbkit 的 `Database`(宿主自己管连接与事务时用)。 */
|
|
8
|
+
export function wrapDatabase(
|
|
9
|
+
dialectName: DialectName,
|
|
10
|
+
ports: {
|
|
11
|
+
iterate(text: string, params: readonly unknown[]): AsyncIterable<unknown>;
|
|
12
|
+
run(text: string, params: readonly unknown[]): Promise<void>;
|
|
13
|
+
/** 宿主自己管事务时提供:把当前事务的执行面做成 scoped `Database`(对应 Go 的 `Tx(ctx, fn)`)。 */
|
|
14
|
+
transaction?: <T>(fn: (scoped: Database) => Promise<T>) => Promise<T>;
|
|
15
|
+
close?(): Promise<void>;
|
|
16
|
+
},
|
|
17
|
+
): Database {
|
|
18
|
+
const database = createDatabase(
|
|
19
|
+
createDialect(dialectName),
|
|
20
|
+
{
|
|
21
|
+
iterate: (text, params) => ports.iterate(text, params),
|
|
22
|
+
run: (text, params) => ports.run(text, params),
|
|
23
|
+
transaction: () => {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"rdbkit: a wrapped database does not own transactions; use the host transaction",
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
close: async () => {
|
|
29
|
+
await ports.close?.();
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
indexCatalog([]),
|
|
33
|
+
);
|
|
34
|
+
return ports.transaction === undefined
|
|
35
|
+
? database
|
|
36
|
+
: { ...database, transaction: ports.transaction };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 结构迁移读取现状用的最小查询面。 */
|
|
40
|
+
export function shapeReader(database: Database): {
|
|
41
|
+
query<T>(sql: string, params?: readonly unknown[]): Promise<T[]>;
|
|
42
|
+
} {
|
|
43
|
+
return {
|
|
44
|
+
async query<T>(sql: string, params: readonly unknown[] = []): Promise<T[]> {
|
|
45
|
+
return await database.read<T>(fragment(sql, params));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/defs.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { TableHandle } from "./table/handle.ts";
|
|
2
|
+
import type { Database } from "./database/types.ts";
|
|
3
|
+
// 表定义就是数据库结构的唯一来源:DDL、启动迁移、类型化的表句柄都从它算出来,
|
|
4
|
+
// 所以再加一张表 / 一列时不需要另写迁移 SQL,也不需要在 sqlite 与 postgres 各写一遍。
|
|
5
|
+
|
|
6
|
+
/** 列类型:只保留跨方言能一一映射的那几种。 */
|
|
7
|
+
export type ColumnTypeName = "serial" | "integer" | "bigint" | "text" | "boolean";
|
|
8
|
+
|
|
9
|
+
/** 外键的删除动作(与 SQL 的 ON DELETE 同名)。 */
|
|
10
|
+
export type DeleteAction = "cascade" | "set null" | "restrict" | "no action";
|
|
11
|
+
|
|
12
|
+
/** 一列的定义。 */
|
|
13
|
+
export interface ColumnDef {
|
|
14
|
+
type: ColumnTypeName;
|
|
15
|
+
notNull?: boolean;
|
|
16
|
+
/** 单列主键;复合主键写在表级的 `primaryKey` 上。 */
|
|
17
|
+
primaryKey?: boolean;
|
|
18
|
+
default?: string | number;
|
|
19
|
+
unique?: boolean;
|
|
20
|
+
references?: {
|
|
21
|
+
table: string;
|
|
22
|
+
column: string;
|
|
23
|
+
onDelete?: DeleteAction;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 表级约束与索引。 */
|
|
28
|
+
export interface CheckDef {
|
|
29
|
+
expression: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UniqueDef {
|
|
33
|
+
columns: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IndexDef {
|
|
37
|
+
columns: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 一张表的完整定义。 */
|
|
41
|
+
export interface TableDef {
|
|
42
|
+
name: string;
|
|
43
|
+
columns: Record<string, ColumnDef>;
|
|
44
|
+
/** 复合主键(按顺序);单列主键用列上的 `primaryKey`。 */
|
|
45
|
+
primaryKey?: readonly string[];
|
|
46
|
+
checks?: Record<string, CheckDef>;
|
|
47
|
+
uniques?: Record<string, UniqueDef>;
|
|
48
|
+
indexes?: Record<string, IndexDef>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 一组表定义(一个库的结构)。 */
|
|
52
|
+
export type Catalog = readonly TableDef[];
|
|
53
|
+
|
|
54
|
+
/** `defineTable` 的返回值:表定义 + 操作钩子(`users.table(database)` 直接拿句柄)。 */
|
|
55
|
+
export type Table<T extends TableDef> = T & {
|
|
56
|
+
table(database: Database): TableHandle<T>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 声明一张表(形状原样保留,另挂一个取句柄的钩子)。 */
|
|
60
|
+
export function defineTable<const T extends TableDef>(def: T): Table<T> {
|
|
61
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(def.name)) {
|
|
62
|
+
throw new Error(`rdbkit: table name '${def.name}' must match [a-z][a-z0-9_]*`);
|
|
63
|
+
}
|
|
64
|
+
const names = Object.keys(def.columns);
|
|
65
|
+
if (names.length === 0) throw new Error(`rdbkit: table '${def.name}' declares no columns`);
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(name)) {
|
|
68
|
+
throw new Error(`rdbkit: table '${def.name}' column '${name}' must match [a-z][a-z0-9_]*`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 定义即操作钩子:`users.table(db)` 等价于 `db.table(users)`,业务侧不需要记两套写法。
|
|
72
|
+
return {
|
|
73
|
+
...def,
|
|
74
|
+
table: (database: Database) => database.table(def) as TableHandle<T>,
|
|
75
|
+
} as Table<T>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 声明一组表。 */
|
|
79
|
+
export function defineCatalog<const T extends readonly TableDef[]>(tables: T): T {
|
|
80
|
+
for (const table of tables) defineTable(table);
|
|
81
|
+
return tables;
|
|
82
|
+
}
|
package/src/dialect.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { ColumnDef, TableDef } from "./defs.ts";
|
|
2
|
+
|
|
3
|
+
/** 支持的方言。 */
|
|
4
|
+
export type DialectName = "sqlite" | "postgres";
|
|
5
|
+
|
|
6
|
+
/** 现有结构的一小块:迁移只关心「有没有这张表 / 这一列 / 这个索引」。 */
|
|
7
|
+
export interface TableShape {
|
|
8
|
+
columns: ReadonlySet<string>;
|
|
9
|
+
indexes: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 一个方言:类型映射、标识符、DDL 片段,以及读现有结构的查询。 */
|
|
13
|
+
export interface Dialect {
|
|
14
|
+
readonly name: DialectName;
|
|
15
|
+
/** 标识符引用(表名 / 列名 / 索引名)。 */
|
|
16
|
+
quote(identifier: string): string;
|
|
17
|
+
/** 带 schema 的表名(sqlite 忽略 schema)。 */
|
|
18
|
+
tableName(table: string, schema?: string): string;
|
|
19
|
+
/** 列类型(含自增主键的特殊写法)。 */
|
|
20
|
+
columnType(column: ColumnDef): string;
|
|
21
|
+
/** 读现有表的列与索引;表不存在返回 undefined。 */
|
|
22
|
+
readShape(database: ShapeReader, table: string, schema?: string): Promise<TableShape | undefined>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 读结构所需的最小执行面(`database/wrap.ts` 的 `shapeReader` 把 `Database` 包成它)。 */
|
|
26
|
+
export interface ShapeReader {
|
|
27
|
+
query<T>(text: string, params?: readonly unknown[]): Promise<T[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SQLITE_KEYWORDS = /^[a-z][a-z0-9_]*$/u;
|
|
31
|
+
|
|
32
|
+
function assertIdentifier(kind: string, name: string): void {
|
|
33
|
+
if (!SQLITE_KEYWORDS.test(name)) throw new Error(`rdbkit: invalid ${kind} '${name}'`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createDialect(name: DialectName): Dialect {
|
|
37
|
+
return name === "sqlite" ? sqliteDialect() : postgresDialect();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sqliteDialect(): Dialect {
|
|
41
|
+
return {
|
|
42
|
+
name: "sqlite",
|
|
43
|
+
quote(identifier) {
|
|
44
|
+
assertIdentifier("identifier", identifier);
|
|
45
|
+
return `"${identifier}"`;
|
|
46
|
+
},
|
|
47
|
+
tableName(table) {
|
|
48
|
+
assertIdentifier("table", table);
|
|
49
|
+
return `"${table}"`;
|
|
50
|
+
},
|
|
51
|
+
columnType(column) {
|
|
52
|
+
// `serial` 在 SQLite 就是「整型主键 + 自增」;其它类型都能直接映射。
|
|
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
|
+
const exists = await database.query<{ name: string }>(
|
|
60
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
61
|
+
[table],
|
|
62
|
+
);
|
|
63
|
+
if (exists.length === 0) return undefined;
|
|
64
|
+
const columns = await database.query<{ name: string }>(`PRAGMA table_info("${table}")`);
|
|
65
|
+
const indexes = await database.query<{ name: string }>(`PRAGMA index_list("${table}")`);
|
|
66
|
+
return {
|
|
67
|
+
columns: new Set(columns.map((row) => row.name)),
|
|
68
|
+
indexes: new Set(indexes.map((row) => row.name)),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function postgresDialect(): Dialect {
|
|
75
|
+
return {
|
|
76
|
+
name: "postgres",
|
|
77
|
+
quote(identifier) {
|
|
78
|
+
assertIdentifier("identifier", identifier);
|
|
79
|
+
return `"${identifier}"`;
|
|
80
|
+
},
|
|
81
|
+
tableName(table, schema = "public") {
|
|
82
|
+
assertIdentifier("schema", schema);
|
|
83
|
+
assertIdentifier("table", table);
|
|
84
|
+
return `"${schema}"."${table}"`;
|
|
85
|
+
},
|
|
86
|
+
columnType(column) {
|
|
87
|
+
switch (column.type) {
|
|
88
|
+
case "serial":
|
|
89
|
+
return "SERIAL";
|
|
90
|
+
case "boolean":
|
|
91
|
+
return "BOOLEAN";
|
|
92
|
+
case "bigint":
|
|
93
|
+
return "BIGINT";
|
|
94
|
+
case "integer":
|
|
95
|
+
return "INTEGER";
|
|
96
|
+
default:
|
|
97
|
+
return "TEXT";
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async readShape(database, table, schema = "public") {
|
|
101
|
+
const columns = await database.query<{ column_name: string }>(
|
|
102
|
+
"SELECT column_name FROM information_schema.columns WHERE table_schema = ? AND table_name = ?",
|
|
103
|
+
[schema, table],
|
|
104
|
+
);
|
|
105
|
+
if (columns.length === 0) return undefined;
|
|
106
|
+
const indexes = await database.query<{ indexname: string }>(
|
|
107
|
+
"SELECT indexname FROM pg_indexes WHERE schemaname = ? AND tablename = ?",
|
|
108
|
+
[schema, table],
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
columns: new Set(columns.map((row) => row.column_name)),
|
|
112
|
+
indexes: new Set(indexes.map((row) => row.indexname)),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 一列的定义 SQL(`"name" TEXT NOT NULL ...`)。 */
|
|
119
|
+
export function columnSql(dialect: Dialect, name: string, column: ColumnDef): string {
|
|
120
|
+
assertIdentifier("column", name);
|
|
121
|
+
const parts = [dialect.quote(name), dialect.columnType(column)];
|
|
122
|
+
if (column.type === "serial" && dialect.name === "sqlite")
|
|
123
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
124
|
+
else if (column.primaryKey === true) parts.push("PRIMARY KEY");
|
|
125
|
+
if (column.notNull === true) parts.push("NOT NULL");
|
|
126
|
+
if (column.unique === true) parts.push("UNIQUE");
|
|
127
|
+
if (column.default !== undefined)
|
|
128
|
+
parts.push(`DEFAULT ${literal(dialect, column.default, column.type)}`);
|
|
129
|
+
if (column.references !== undefined) {
|
|
130
|
+
const { table, column: referenced, onDelete } = column.references;
|
|
131
|
+
assertIdentifier("table", table);
|
|
132
|
+
assertIdentifier("column", referenced);
|
|
133
|
+
parts.push(
|
|
134
|
+
`REFERENCES ${dialect.tableName(table)}(${dialect.quote(referenced)})` +
|
|
135
|
+
(onDelete === undefined ? "" : ` ON DELETE ${onDelete.toUpperCase()}`),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return parts.join(" ");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function literal(dialect: Dialect, value: string | number, type: ColumnDef["type"]): string {
|
|
142
|
+
if (type === "boolean") {
|
|
143
|
+
if (typeof value === "number") {
|
|
144
|
+
return dialect.name === "postgres" ? (value === 0 ? "FALSE" : "TRUE") : String(value);
|
|
145
|
+
}
|
|
146
|
+
return dialect.name === "postgres"
|
|
147
|
+
? value.toUpperCase()
|
|
148
|
+
: /^(true|1)$/iu.test(value)
|
|
149
|
+
? "1"
|
|
150
|
+
: "0";
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "number") return String(value);
|
|
153
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 建表语句(IF NOT EXISTS;列与表级约束都由定义算出来)。 */
|
|
157
|
+
export function createTableSql(dialect: Dialect, table: TableDef, schema?: string): string {
|
|
158
|
+
const parts = Object.entries(table.columns).map(([name, column]) =>
|
|
159
|
+
columnSql(dialect, name, column),
|
|
160
|
+
);
|
|
161
|
+
const primaryKey =
|
|
162
|
+
table.primaryKey === undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: table.primaryKey.map((name) => dialect.quote(name)).join(", ");
|
|
165
|
+
if (primaryKey !== undefined) parts.push(`PRIMARY KEY (${primaryKey})`);
|
|
166
|
+
for (const [name, check] of Object.entries(table.checks ?? {})) {
|
|
167
|
+
assertIdentifier("constraint", name);
|
|
168
|
+
parts.push(`CONSTRAINT ${dialect.quote(name)} CHECK (${check.expression})`);
|
|
169
|
+
}
|
|
170
|
+
for (const [name, unique] of Object.entries(table.uniques ?? {})) {
|
|
171
|
+
assertIdentifier("constraint", name);
|
|
172
|
+
parts.push(
|
|
173
|
+
`CONSTRAINT ${dialect.quote(name)} UNIQUE (${unique.columns
|
|
174
|
+
.map((column) => dialect.quote(column))
|
|
175
|
+
.join(", ")})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return (
|
|
179
|
+
`CREATE TABLE IF NOT EXISTS ${dialect.tableName(table.name, schema)} (\n ` +
|
|
180
|
+
`${parts.join(",\n ")}\n)`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 索引语句(`unique` 时是唯一索引)。 */
|
|
185
|
+
export function createIndexSql(
|
|
186
|
+
dialect: Dialect,
|
|
187
|
+
table: TableDef,
|
|
188
|
+
name: string,
|
|
189
|
+
columns: readonly string[],
|
|
190
|
+
unique = false,
|
|
191
|
+
): string {
|
|
192
|
+
assertIdentifier("index", name);
|
|
193
|
+
return (
|
|
194
|
+
`CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${dialect.quote(name)} ` +
|
|
195
|
+
`ON ${dialect.tableName(table.name)} (${columns.map((column) => dialect.quote(column)).join(", ")})`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 删列(只在显式允许破坏性变更时用)。 */
|
|
200
|
+
export function dropColumnSql(dialect: Dialect, table: string, name: string): string {
|
|
201
|
+
assertIdentifier("column", name);
|
|
202
|
+
return `ALTER TABLE ${dialect.tableName(table)} DROP COLUMN ${dialect.quote(name)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 删索引(只在显式允许破坏性变更时用)。 */
|
|
206
|
+
export function dropIndexSql(dialect: Dialect, name: string): string {
|
|
207
|
+
assertIdentifier("index", name);
|
|
208
|
+
return `DROP INDEX IF EXISTS ${dialect.quote(name)}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 加列语句(启动迁移只做「缺列则补」,不改类型、不删列)。 */
|
|
212
|
+
export function addColumnSql(
|
|
213
|
+
dialect: Dialect,
|
|
214
|
+
table: string,
|
|
215
|
+
name: string,
|
|
216
|
+
column: ColumnDef,
|
|
217
|
+
): string {
|
|
218
|
+
return `ALTER TABLE ${dialect.tableName(table)} ADD COLUMN ${columnSql(dialect, name, column)}`;
|
|
219
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DialectName } from "./dialect.ts";
|
|
2
|
+
|
|
3
|
+
/** 数据库错误的可判定类别(调用方据此决定重试 / 上报 / 走业务分支)。 */
|
|
4
|
+
export type DbErrorKind =
|
|
5
|
+
| "unique"
|
|
6
|
+
| "foreign-key"
|
|
7
|
+
| "not-null"
|
|
8
|
+
| "check"
|
|
9
|
+
| "timeout"
|
|
10
|
+
| "connection"
|
|
11
|
+
| "unknown";
|
|
12
|
+
|
|
13
|
+
/** 统一的数据库错误:`kind` 是方言无关的判定,原始错误在 `cause`。 */
|
|
14
|
+
export class DbError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly kind: DbErrorKind,
|
|
17
|
+
message: string,
|
|
18
|
+
options?: { cause?: unknown },
|
|
19
|
+
) {
|
|
20
|
+
super(message, options);
|
|
21
|
+
this.name = "DbError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 把驱动错误翻译成 {@link DbErrorKind}:SQLite 用错误文本前缀,PostgreSQL 用 SQLSTATE。
|
|
27
|
+
* @param error - 驱动抛出的错误。
|
|
28
|
+
* @param dialect - 产生它的方言。
|
|
29
|
+
* @returns 已分类的错误(原错误在 `cause`)。
|
|
30
|
+
*/
|
|
31
|
+
export function classifyError(error: unknown, dialect: DialectName): DbError {
|
|
32
|
+
if (error instanceof DbError) return error;
|
|
33
|
+
const cause = error as { code?: unknown; message?: unknown } | undefined;
|
|
34
|
+
const code = typeof cause?.code === "string" ? cause.code : undefined;
|
|
35
|
+
const message = typeof cause?.message === "string" ? cause.message : String(error);
|
|
36
|
+
|
|
37
|
+
if (dialect === "postgres") {
|
|
38
|
+
switch (code) {
|
|
39
|
+
case "23505":
|
|
40
|
+
return new DbError("unique", message, { cause: error });
|
|
41
|
+
case "23503":
|
|
42
|
+
return new DbError("foreign-key", message, { cause: error });
|
|
43
|
+
case "23502":
|
|
44
|
+
return new DbError("not-null", message, { cause: error });
|
|
45
|
+
case "23514":
|
|
46
|
+
return new DbError("check", message, { cause: error });
|
|
47
|
+
case "57014":
|
|
48
|
+
case "55P03":
|
|
49
|
+
return new DbError("timeout", message, { cause: error });
|
|
50
|
+
case "53300":
|
|
51
|
+
return new DbError("connection", message, { cause: error });
|
|
52
|
+
default:
|
|
53
|
+
if (code !== undefined && code.startsWith("08")) {
|
|
54
|
+
return new DbError("connection", message, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
return new DbError("unknown", message, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (/UNIQUE constraint failed/iu.test(message))
|
|
61
|
+
return new DbError("unique", message, { cause: error });
|
|
62
|
+
if (/FOREIGN KEY constraint failed/iu.test(message))
|
|
63
|
+
return new DbError("foreign-key", message, { cause: error });
|
|
64
|
+
if (/NOT NULL constraint failed/iu.test(message))
|
|
65
|
+
return new DbError("not-null", message, { cause: error });
|
|
66
|
+
if (/CHECK constraint failed/iu.test(message))
|
|
67
|
+
return new DbError("check", message, { cause: error });
|
|
68
|
+
if (/SQLITE_BUSY|database is locked|database table is locked/iu.test(message)) {
|
|
69
|
+
return new DbError("timeout", message, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
return new DbError("unknown", message, { cause: error });
|
|
72
|
+
}
|