@zhin.js/database 1.1.1 → 1.1.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/lib/base/dialect.d.ts +5 -1
- package/lib/base/dialect.js +16 -0
- package/lib/dialects/memory.d.ts +4 -2
- package/lib/dialects/memory.js +10 -3
- package/lib/dialects/mongodb.d.ts +1 -0
- package/lib/dialects/mongodb.js +21 -1
- package/lib/dialects/mysql.d.ts +8 -1
- package/lib/dialects/mysql.js +92 -20
- package/lib/dialects/pg.d.ts +3 -1
- package/lib/dialects/pg.js +67 -9
- package/lib/dialects/redis.js +2 -1
- package/lib/dialects/sqlite.js +5 -2
- package/lib/migration.js +5 -50
- package/lib/type/document/database.js +8 -8
- package/lib/type/document/model.js +1 -1
- package/lib/type/keyvalue/model.js +16 -37
- package/lib/type/related/database.d.ts +1 -1
- package/lib/type/related/database.js +17 -24
- package/lib/types.d.ts +1 -1
- package/package.json +2 -2
package/lib/base/dialect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transaction, TransactionOptions, IsolationLevel } from '../types.js';
|
|
1
|
+
import { Transaction, TransactionOptions, IsolationLevel, type Column, type Definition } from '../types.js';
|
|
2
2
|
export declare abstract class Dialect<T, S extends Record<string, object>, Q> {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly config: T;
|
|
@@ -25,6 +25,10 @@ export declare abstract class Dialect<T, S extends Record<string, object>, Q> {
|
|
|
25
25
|
abstract formatDropTable<T extends keyof S>(tableName: T, ifExists?: boolean): string;
|
|
26
26
|
abstract formatDropIndex<T extends keyof S>(indexName: string, tableName: T, ifExists?: boolean): string;
|
|
27
27
|
abstract dispose(): Promise<void>;
|
|
28
|
+
/** Format one column while preserving dialect-specific identity syntax. */
|
|
29
|
+
formatColumnDefinition(field: string, column: Column<any>): string;
|
|
30
|
+
/** Apply widening-only schema updates that CREATE TABLE IF NOT EXISTS cannot. */
|
|
31
|
+
reconcileDefinitions(_definitions: ReadonlyMap<keyof S, Definition<S[keyof S]>>): Promise<void>;
|
|
28
32
|
/**
|
|
29
33
|
* 是否支持事务
|
|
30
34
|
*/
|
package/lib/base/dialect.js
CHANGED
|
@@ -11,6 +11,22 @@ export class Dialect {
|
|
|
11
11
|
this.name = name;
|
|
12
12
|
this.config = config;
|
|
13
13
|
}
|
|
14
|
+
/** Format one column while preserving dialect-specific identity syntax. */
|
|
15
|
+
formatColumnDefinition(field, column) {
|
|
16
|
+
const name = this.quoteIdentifier(field);
|
|
17
|
+
const type = this.mapColumnType(column.type);
|
|
18
|
+
const length = column.length ? `(${column.length})` : '';
|
|
19
|
+
const nullable = column.nullable === false ? ' NOT NULL' : '';
|
|
20
|
+
const primary = column.primary ? ' PRIMARY KEY' : '';
|
|
21
|
+
const unique = column.unique ? ' UNIQUE' : '';
|
|
22
|
+
const autoIncrement = column.autoIncrement ? ' AUTOINCREMENT' : '';
|
|
23
|
+
const defaultValue = column.default !== undefined
|
|
24
|
+
? ` DEFAULT ${this.formatDefaultValue(column.default)}`
|
|
25
|
+
: '';
|
|
26
|
+
return `${name} ${type}${length}${primary}${unique}${autoIncrement}${nullable}${defaultValue}`;
|
|
27
|
+
}
|
|
28
|
+
/** Apply widening-only schema updates that CREATE TABLE IF NOT EXISTS cannot. */
|
|
29
|
+
async reconcileDefinitions(_definitions) { }
|
|
14
30
|
// ============================================================================
|
|
15
31
|
// Transaction Support (optional, default implementations)
|
|
16
32
|
// ============================================================================
|
package/lib/dialects/memory.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Dialect } from '../base/index.js';
|
|
1
|
+
import { Database, Dialect } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
3
|
export interface MemoryConfig {
|
|
4
4
|
}
|
|
@@ -6,6 +6,7 @@ export declare class MemoryDialect<S extends Record<string, object>> extends Dia
|
|
|
6
6
|
private connected;
|
|
7
7
|
private tables;
|
|
8
8
|
private autoIncrementCounters;
|
|
9
|
+
private declaredSchemas;
|
|
9
10
|
constructor(config?: MemoryConfig);
|
|
10
11
|
isConnected(): boolean;
|
|
11
12
|
connect(): Promise<void>;
|
|
@@ -13,6 +14,7 @@ export declare class MemoryDialect<S extends Record<string, object>> extends Dia
|
|
|
13
14
|
healthCheck(): Promise<boolean>;
|
|
14
15
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
15
16
|
dispose(): Promise<void>;
|
|
17
|
+
registerTableSchema(table: string, definition: Record<string, any>): void;
|
|
16
18
|
mapColumnType(type: string): string;
|
|
17
19
|
quoteIdentifier(identifier: string | symbol | number): string;
|
|
18
20
|
getParameterPlaceholder(index: number): string;
|
|
@@ -52,5 +54,5 @@ export declare class MemoryDialect<S extends Record<string, object>> extends Dia
|
|
|
52
54
|
private applyOrderBy;
|
|
53
55
|
}
|
|
54
56
|
export declare class Memory<S extends Record<string, object>> extends RelatedDatabase<MemoryConfig, S> {
|
|
55
|
-
constructor(config?: MemoryConfig);
|
|
57
|
+
constructor(config?: MemoryConfig, definitions?: Database.DefinitionObj<S>);
|
|
56
58
|
}
|
package/lib/dialects/memory.js
CHANGED
|
@@ -57,6 +57,7 @@ export class MemoryDialect extends Dialect {
|
|
|
57
57
|
connected = false;
|
|
58
58
|
tables = new Map();
|
|
59
59
|
autoIncrementCounters = new Map();
|
|
60
|
+
declaredSchemas = new Map();
|
|
60
61
|
constructor(config = {}) {
|
|
61
62
|
super('memory', config);
|
|
62
63
|
}
|
|
@@ -115,12 +116,16 @@ export class MemoryDialect extends Dialect {
|
|
|
115
116
|
async dispose() {
|
|
116
117
|
await this.disconnect();
|
|
117
118
|
}
|
|
119
|
+
registerTableSchema(table, definition) {
|
|
120
|
+
this.declaredSchemas.set(table, structuredClone(definition));
|
|
121
|
+
}
|
|
118
122
|
// SQL generation methods
|
|
119
123
|
mapColumnType(type) {
|
|
120
124
|
// 内存数据库不需要严格的类型映射,但保持一致性
|
|
121
125
|
const typeMap = {
|
|
122
126
|
'text': 'TEXT',
|
|
123
127
|
'integer': 'INTEGER',
|
|
128
|
+
'bigint': 'INTEGER',
|
|
124
129
|
'float': 'REAL',
|
|
125
130
|
'boolean': 'BOOLEAN',
|
|
126
131
|
'date': 'DATE',
|
|
@@ -202,12 +207,14 @@ export class MemoryDialect extends Dialect {
|
|
|
202
207
|
const tableName = match[1];
|
|
203
208
|
const columnsStr = match[2];
|
|
204
209
|
// 解析列定义
|
|
205
|
-
const schema = {};
|
|
210
|
+
const schema = structuredClone(this.declaredSchemas.get(tableName) ?? {});
|
|
206
211
|
const columnDefs = columnsStr.split(',').map(col => col.trim());
|
|
207
212
|
for (const colDef of columnDefs) {
|
|
208
213
|
const parts = colDef.split(/\s+/);
|
|
209
214
|
const columnName = parts[0].replace(/"/g, '');
|
|
210
215
|
const columnType = parts[1];
|
|
216
|
+
if (schema[columnName])
|
|
217
|
+
continue;
|
|
211
218
|
schema[columnName] = {
|
|
212
219
|
type: columnType,
|
|
213
220
|
primary: colDef.toLowerCase().includes('primary key'),
|
|
@@ -820,8 +827,8 @@ export class MemoryDialect extends Dialect {
|
|
|
820
827
|
}
|
|
821
828
|
}
|
|
822
829
|
export class Memory extends RelatedDatabase {
|
|
823
|
-
constructor(config = {}) {
|
|
824
|
-
super(new MemoryDialect(config));
|
|
830
|
+
constructor(config = {}, definitions) {
|
|
831
|
+
super(new MemoryDialect(config), definitions);
|
|
825
832
|
}
|
|
826
833
|
}
|
|
827
834
|
Registry.register('memory', Memory);
|
|
@@ -81,6 +81,7 @@ export declare class MongoDBDialect<S extends Record<string, object> = Record<st
|
|
|
81
81
|
private executeCreateIndex;
|
|
82
82
|
private executeDropIndex;
|
|
83
83
|
private executeDropCollection;
|
|
84
|
+
private executeCreateCollection;
|
|
84
85
|
get dialectInfo(): DatabaseDialect;
|
|
85
86
|
}
|
|
86
87
|
export declare class MongoDB<S extends Record<string, object> = Record<string, object>> extends DocumentDatabase<MongoDBDialectConfig, S> {
|
package/lib/dialects/mongodb.js
CHANGED
|
@@ -49,7 +49,8 @@ export class MongoDBDialect extends Dialect {
|
|
|
49
49
|
try {
|
|
50
50
|
// 动态导入 mongodb 客户端
|
|
51
51
|
const { MongoClient } = await import('mongodb');
|
|
52
|
-
|
|
52
|
+
const { url, dbName: _dbName, ...clientOptions } = this.config;
|
|
53
|
+
this.client = new MongoClient(url, clientOptions);
|
|
53
54
|
await this.client.connect();
|
|
54
55
|
this.db = this.client.db(this.config.dbName);
|
|
55
56
|
}
|
|
@@ -105,6 +106,8 @@ export class MongoDBDialect extends Dialect {
|
|
|
105
106
|
return await this.executeDropIndex(collection, query, params);
|
|
106
107
|
case 'dropCollection':
|
|
107
108
|
return await this.executeDropCollection(collection);
|
|
109
|
+
case 'createCollection':
|
|
110
|
+
return await this.executeCreateCollection(query.collection);
|
|
108
111
|
default:
|
|
109
112
|
throw new Error(`不支持的 MongoDB 操作: ${query.operation}`);
|
|
110
113
|
}
|
|
@@ -145,6 +148,8 @@ export class MongoDBDialect extends Dialect {
|
|
|
145
148
|
return 'String';
|
|
146
149
|
case 'integer':
|
|
147
150
|
return 'Int32';
|
|
151
|
+
case 'bigint':
|
|
152
|
+
return 'Double';
|
|
148
153
|
case 'float':
|
|
149
154
|
return 'Double';
|
|
150
155
|
case 'boolean':
|
|
@@ -427,6 +432,20 @@ export class MongoDBDialect extends Dialect {
|
|
|
427
432
|
const result = await collection.drop();
|
|
428
433
|
return [{ result }];
|
|
429
434
|
}
|
|
435
|
+
async executeCreateCollection(name) {
|
|
436
|
+
const existing = await this.db.listCollections({ name }, { nameOnly: true }).hasNext();
|
|
437
|
+
if (!existing) {
|
|
438
|
+
try {
|
|
439
|
+
await this.db.createCollection(name);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
const duplicate = error;
|
|
443
|
+
if (duplicate.code !== 48 && duplicate.codeName !== 'NamespaceExists')
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
430
449
|
get dialectInfo() {
|
|
431
450
|
return {
|
|
432
451
|
name: this.name,
|
|
@@ -442,6 +461,7 @@ export class MongoDBDialect extends Dialect {
|
|
|
442
461
|
dataTypes: {
|
|
443
462
|
'string': 'String',
|
|
444
463
|
'integer': 'Int32',
|
|
464
|
+
'bigint': 'Double',
|
|
445
465
|
'float': 'Double',
|
|
446
466
|
'boolean': 'Boolean',
|
|
447
467
|
'date': 'Date',
|
package/lib/dialects/mysql.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Dialect, Database } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
3
|
import type { ConnectionOptions } from 'mysql2/promise';
|
|
4
|
-
import { Column, Transaction, TransactionOptions, PoolConfig } from "../types.js";
|
|
4
|
+
import { Column, Transaction, TransactionOptions, PoolConfig, type Definition } from "../types.js";
|
|
5
5
|
export interface MySQLDialectConfig extends ConnectionOptions {
|
|
6
6
|
/**
|
|
7
7
|
* 连接池配置
|
|
@@ -13,12 +13,18 @@ export declare class MySQLDialect<S extends Record<string, object> = Record<stri
|
|
|
13
13
|
private connection;
|
|
14
14
|
private pool;
|
|
15
15
|
private usePool;
|
|
16
|
+
private readonly booleanColumns;
|
|
17
|
+
private readonly bigintColumns;
|
|
16
18
|
constructor(config: MySQLDialectConfig);
|
|
17
19
|
isConnected(): boolean;
|
|
18
20
|
connect(): Promise<void>;
|
|
19
21
|
disconnect(): Promise<void>;
|
|
20
22
|
healthCheck(): Promise<boolean>;
|
|
21
23
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
24
|
+
registerTableSchema(table: string, definition: Record<string, {
|
|
25
|
+
type: string;
|
|
26
|
+
}>): void;
|
|
27
|
+
private processQueryResults;
|
|
22
28
|
dispose(): Promise<void>;
|
|
23
29
|
/**
|
|
24
30
|
* 获取连接池统计信息(仅在使用连接池时有效)
|
|
@@ -42,6 +48,7 @@ export declare class MySQLDialect<S extends Record<string, object> = Record<stri
|
|
|
42
48
|
formatLimitOffset(limit: number, offset: number): string;
|
|
43
49
|
formatCreateTable<T extends keyof S>(tableName: T, columns: string[]): string;
|
|
44
50
|
formatColumnDefinition(field: string, column: Column<any>): string;
|
|
51
|
+
reconcileDefinitions(definitions: ReadonlyMap<keyof S, Definition<S[keyof S]>>): Promise<void>;
|
|
45
52
|
formatAlterTable<T extends keyof S>(tableName: T, alterations: string[]): string;
|
|
46
53
|
formatDropTable<T extends keyof S>(tableName: T, ifExists?: boolean): string;
|
|
47
54
|
formatDropIndex<T extends keyof S>(indexName: string, tableName: T, ifExists?: boolean): string;
|
package/lib/dialects/mysql.js
CHANGED
|
@@ -7,6 +7,8 @@ export class MySQLDialect extends Dialect {
|
|
|
7
7
|
connection = null;
|
|
8
8
|
pool = null;
|
|
9
9
|
usePool = false;
|
|
10
|
+
booleanColumns = new Set();
|
|
11
|
+
bigintColumns = new Set();
|
|
10
12
|
constructor(config) {
|
|
11
13
|
super('mysql', config);
|
|
12
14
|
this.usePool = !!config.pool;
|
|
@@ -19,12 +21,13 @@ export class MySQLDialect extends Dialect {
|
|
|
19
21
|
try {
|
|
20
22
|
if (this.usePool) {
|
|
21
23
|
const { createPool } = await import('mysql2/promise');
|
|
24
|
+
const { pool, ...connectionOptions } = this.config;
|
|
22
25
|
const poolConfig = {
|
|
23
|
-
...
|
|
26
|
+
...connectionOptions,
|
|
24
27
|
waitForConnections: true,
|
|
25
|
-
connectionLimit:
|
|
28
|
+
connectionLimit: pool?.max ?? 10,
|
|
26
29
|
queueLimit: 0,
|
|
27
|
-
idleTimeout:
|
|
30
|
+
idleTimeout: pool?.idleTimeoutMillis ?? 60000,
|
|
28
31
|
};
|
|
29
32
|
this.pool = createPool(poolConfig);
|
|
30
33
|
logger.info(`MySQL 连接池已创建 (max: ${poolConfig.connectionLimit})`);
|
|
@@ -63,14 +66,48 @@ export class MySQLDialect extends Dialect {
|
|
|
63
66
|
}
|
|
64
67
|
async query(sql, params) {
|
|
65
68
|
if (this.usePool) {
|
|
66
|
-
const [rows] = await this.pool.execute(sql, params);
|
|
67
|
-
return rows;
|
|
69
|
+
const [rows, fields] = await this.pool.execute(sql, params);
|
|
70
|
+
return this.processQueryResults(rows, fields);
|
|
68
71
|
}
|
|
69
72
|
else {
|
|
70
|
-
const [rows] = await this.connection.execute(sql, params);
|
|
71
|
-
return rows;
|
|
73
|
+
const [rows, fields] = await this.connection.execute(sql, params);
|
|
74
|
+
return this.processQueryResults(rows, fields);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
77
|
+
registerTableSchema(table, definition) {
|
|
78
|
+
for (const [columnName, column] of Object.entries(definition)) {
|
|
79
|
+
if (column.type === 'boolean')
|
|
80
|
+
this.booleanColumns.add(`${table}.${columnName}`);
|
|
81
|
+
if (column.type === 'bigint')
|
|
82
|
+
this.bigintColumns.add(`${table}.${columnName}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
processQueryResults(data, fields) {
|
|
86
|
+
if (!Array.isArray(data))
|
|
87
|
+
return data;
|
|
88
|
+
return data.map((row) => {
|
|
89
|
+
if (!row || typeof row !== 'object')
|
|
90
|
+
return row;
|
|
91
|
+
const normalized = { ...row };
|
|
92
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
93
|
+
const table = String(field.orgTable ?? field.table ?? '');
|
|
94
|
+
const column = String(field.orgName ?? field.name ?? '');
|
|
95
|
+
const outputName = String(field.name ?? column);
|
|
96
|
+
if (this.booleanColumns.has(`${table}.${column}`) &&
|
|
97
|
+
typeof normalized[outputName] === 'number') {
|
|
98
|
+
normalized[outputName] = normalized[outputName] !== 0;
|
|
99
|
+
}
|
|
100
|
+
if (this.bigintColumns.has(`${table}.${column}`) &&
|
|
101
|
+
typeof normalized[outputName] === 'string' &&
|
|
102
|
+
/^-?\d+$/u.test(normalized[outputName])) {
|
|
103
|
+
const number = Number(normalized[outputName]);
|
|
104
|
+
if (Number.isSafeInteger(number))
|
|
105
|
+
normalized[outputName] = number;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return normalized;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
74
111
|
async dispose() {
|
|
75
112
|
await this.disconnect();
|
|
76
113
|
}
|
|
@@ -91,6 +128,7 @@ export class MySQLDialect extends Dialect {
|
|
|
91
128
|
const typeMap = {
|
|
92
129
|
'text': 'TEXT',
|
|
93
130
|
'integer': 'INT',
|
|
131
|
+
'bigint': 'BIGINT',
|
|
94
132
|
'float': 'FLOAT',
|
|
95
133
|
'boolean': 'BOOLEAN',
|
|
96
134
|
'date': 'DATETIME',
|
|
@@ -164,6 +202,39 @@ export class MySQLDialect extends Dialect {
|
|
|
164
202
|
: '';
|
|
165
203
|
return `${name} ${type}${length}${primary}${unique}${autoIncrement}${nullable}${defaultVal}`;
|
|
166
204
|
}
|
|
205
|
+
async reconcileDefinitions(definitions) {
|
|
206
|
+
const expected = new Map();
|
|
207
|
+
for (const [tableName, definition] of definitions) {
|
|
208
|
+
for (const [columnName, column] of Object.entries(definition)) {
|
|
209
|
+
if (column.type === 'bigint' || column.autoIncrement) {
|
|
210
|
+
expected.set(`${String(tableName)}\0${columnName}`, column);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (expected.size === 0)
|
|
215
|
+
return;
|
|
216
|
+
const tableNames = [...new Set([...expected.keys()].map((key) => key.slice(0, key.indexOf('\0'))))];
|
|
217
|
+
const placeholders = tableNames.map(() => '?').join(', ');
|
|
218
|
+
const rows = await this.query(`SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type, EXTRA AS extra
|
|
219
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
220
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${placeholders})`, tableNames);
|
|
221
|
+
const narrowTypes = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer']);
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
const column = expected.get(`${row.table_name}\0${row.column_name}`);
|
|
224
|
+
if (!column)
|
|
225
|
+
continue;
|
|
226
|
+
const needsBigint = column.type === 'bigint' && narrowTypes.has(row.data_type.toLowerCase());
|
|
227
|
+
const needsAutoIncrement = Boolean(column.autoIncrement) && !row.extra.toLowerCase().includes('auto_increment');
|
|
228
|
+
if (!needsBigint && !needsAutoIncrement)
|
|
229
|
+
continue;
|
|
230
|
+
const definition = this.formatColumnDefinition(row.column_name, {
|
|
231
|
+
...column,
|
|
232
|
+
primary: false,
|
|
233
|
+
unique: false,
|
|
234
|
+
});
|
|
235
|
+
await this.query(`ALTER TABLE ${this.quoteIdentifier(row.table_name)} MODIFY COLUMN ${definition}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
167
238
|
formatAlterTable(tableName, alterations) {
|
|
168
239
|
return `ALTER TABLE ${this.quoteIdentifier(String(tableName))} ${alterations.join(', ')}`;
|
|
169
240
|
}
|
|
@@ -189,19 +260,20 @@ export class MySQLDialect extends Dialect {
|
|
|
189
260
|
* 在连接池模式下,会获取一个专用连接用于事务
|
|
190
261
|
*/
|
|
191
262
|
async beginTransaction(options) {
|
|
263
|
+
const dialect = this;
|
|
192
264
|
if (this.usePool) {
|
|
193
265
|
// 从连接池获取一个连接用于事务
|
|
194
266
|
const connection = await this.pool.getConnection();
|
|
195
267
|
// 设置隔离级别
|
|
196
268
|
if (options?.isolationLevel) {
|
|
197
|
-
await connection.
|
|
269
|
+
await connection.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
|
|
198
270
|
}
|
|
199
271
|
// 开始事务
|
|
200
|
-
await connection.
|
|
272
|
+
await connection.beginTransaction();
|
|
201
273
|
return {
|
|
202
274
|
async commit() {
|
|
203
275
|
try {
|
|
204
|
-
await connection.
|
|
276
|
+
await connection.commit();
|
|
205
277
|
}
|
|
206
278
|
finally {
|
|
207
279
|
connection.release(); // 归还连接到池
|
|
@@ -209,36 +281,36 @@ export class MySQLDialect extends Dialect {
|
|
|
209
281
|
},
|
|
210
282
|
async rollback() {
|
|
211
283
|
try {
|
|
212
|
-
await connection.
|
|
284
|
+
await connection.rollback();
|
|
213
285
|
}
|
|
214
286
|
finally {
|
|
215
287
|
connection.release(); // 归还连接到池
|
|
216
288
|
}
|
|
217
289
|
},
|
|
218
290
|
async query(sql, params) {
|
|
219
|
-
const [rows] = await connection.execute(sql, params);
|
|
220
|
-
return rows;
|
|
291
|
+
const [rows, fields] = await connection.execute(sql, params);
|
|
292
|
+
return dialect.processQueryResults(rows, fields);
|
|
221
293
|
}
|
|
222
294
|
};
|
|
223
295
|
}
|
|
224
296
|
else {
|
|
225
|
-
// 单连接模式
|
|
226
|
-
const dialect = this;
|
|
227
297
|
// 设置隔离级别
|
|
228
298
|
if (options?.isolationLevel) {
|
|
229
|
-
await this.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
|
|
299
|
+
await this.connection.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
|
|
230
300
|
}
|
|
231
301
|
// 开始事务
|
|
232
|
-
await this.
|
|
302
|
+
await this.connection.beginTransaction();
|
|
303
|
+
const connection = this.connection;
|
|
233
304
|
return {
|
|
234
305
|
async commit() {
|
|
235
|
-
await
|
|
306
|
+
await connection.commit();
|
|
236
307
|
},
|
|
237
308
|
async rollback() {
|
|
238
|
-
await
|
|
309
|
+
await connection.rollback();
|
|
239
310
|
},
|
|
240
311
|
async query(sql, params) {
|
|
241
|
-
|
|
312
|
+
const [rows, fields] = await connection.execute(sql, params);
|
|
313
|
+
return dialect.processQueryResults(rows, fields);
|
|
242
314
|
}
|
|
243
315
|
};
|
|
244
316
|
}
|
package/lib/dialects/pg.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Dialect, Database } from '../base/index.js';
|
|
2
2
|
import { RelatedDatabase } from "../type/related/database.js";
|
|
3
3
|
import type { ClientConfig } from 'pg';
|
|
4
|
-
import { Column, Transaction, TransactionOptions, PoolConfig } from "../types.js";
|
|
4
|
+
import { Column, Transaction, TransactionOptions, PoolConfig, type Definition } from "../types.js";
|
|
5
5
|
export interface PostgreSQLDialectConfig extends ClientConfig {
|
|
6
6
|
/**
|
|
7
7
|
* 连接池配置
|
|
@@ -19,6 +19,7 @@ export declare class PostgreSQLDialect<S extends Record<string, object> = Record
|
|
|
19
19
|
disconnect(): Promise<void>;
|
|
20
20
|
healthCheck(): Promise<boolean>;
|
|
21
21
|
query<U = any>(sql: string, params?: any[]): Promise<U>;
|
|
22
|
+
private normalizeResult;
|
|
22
23
|
dispose(): Promise<void>;
|
|
23
24
|
/**
|
|
24
25
|
* 获取连接池统计信息(仅在使用连接池时有效)
|
|
@@ -42,6 +43,7 @@ export declare class PostgreSQLDialect<S extends Record<string, object> = Record
|
|
|
42
43
|
formatLimitOffset(limit: number, offset: number): string;
|
|
43
44
|
formatCreateTable<T extends keyof S>(tableName: T, columns: string[]): string;
|
|
44
45
|
formatColumnDefinition(field: string, column: Column<any>): string;
|
|
46
|
+
reconcileDefinitions(definitions: ReadonlyMap<keyof S, Definition<S[keyof S]>>): Promise<void>;
|
|
45
47
|
formatAlterTable<T extends keyof S>(tableName: T, alterations: string[]): string;
|
|
46
48
|
formatDropTable<T extends keyof S>(tableName: T, ifExists?: boolean): string;
|
|
47
49
|
formatDropIndex<T extends keyof S>(indexName: string, tableName: T, ifExists?: boolean): string;
|
package/lib/dialects/pg.js
CHANGED
|
@@ -19,12 +19,13 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
19
19
|
try {
|
|
20
20
|
if (this.usePool) {
|
|
21
21
|
const { Pool } = await import('pg');
|
|
22
|
+
const { pool, ...connectionOptions } = this.config;
|
|
22
23
|
const poolConfig = {
|
|
23
|
-
...
|
|
24
|
-
max:
|
|
25
|
-
min:
|
|
26
|
-
idleTimeoutMillis:
|
|
27
|
-
connectionTimeoutMillis:
|
|
24
|
+
...connectionOptions,
|
|
25
|
+
max: pool?.max ?? 10,
|
|
26
|
+
min: pool?.min ?? 2,
|
|
27
|
+
idleTimeoutMillis: pool?.idleTimeoutMillis ?? 30000,
|
|
28
|
+
connectionTimeoutMillis: pool?.acquireTimeoutMillis ?? 10000,
|
|
28
29
|
};
|
|
29
30
|
this.pool = new Pool(poolConfig);
|
|
30
31
|
logger.info(`PostgreSQL 连接池已创建 (max: ${poolConfig.max})`);
|
|
@@ -65,13 +66,32 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
65
66
|
async query(sql, params) {
|
|
66
67
|
if (this.usePool) {
|
|
67
68
|
const result = await this.pool.query(sql, params);
|
|
68
|
-
return result
|
|
69
|
+
return this.normalizeResult(result);
|
|
69
70
|
}
|
|
70
71
|
else {
|
|
71
72
|
const result = await this.connection.query(sql, params);
|
|
72
|
-
return result
|
|
73
|
+
return this.normalizeResult(result);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
normalizeResult(result) {
|
|
77
|
+
const bigintFields = new Set((result.fields ?? [])
|
|
78
|
+
.filter((field) => field.dataTypeID === 20)
|
|
79
|
+
.map((field) => field.name));
|
|
80
|
+
if (bigintFields.size === 0)
|
|
81
|
+
return result.rows;
|
|
82
|
+
return result.rows.map((row) => {
|
|
83
|
+
const normalized = { ...row };
|
|
84
|
+
for (const field of bigintFields) {
|
|
85
|
+
const value = normalized[field];
|
|
86
|
+
if (typeof value !== 'string' || !/^-?\d+$/u.test(value))
|
|
87
|
+
continue;
|
|
88
|
+
const number = Number(value);
|
|
89
|
+
if (Number.isSafeInteger(number))
|
|
90
|
+
normalized[field] = number;
|
|
91
|
+
}
|
|
92
|
+
return normalized;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
75
95
|
async dispose() {
|
|
76
96
|
await this.disconnect();
|
|
77
97
|
}
|
|
@@ -92,6 +112,7 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
92
112
|
const typeMap = {
|
|
93
113
|
'text': 'TEXT',
|
|
94
114
|
'integer': 'INTEGER',
|
|
115
|
+
'bigint': 'BIGINT',
|
|
95
116
|
'float': 'REAL',
|
|
96
117
|
'boolean': 'BOOLEAN',
|
|
97
118
|
'date': 'TIMESTAMP',
|
|
@@ -159,10 +180,46 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
159
180
|
const nullable = column.nullable === false ? ' NOT NULL' : '';
|
|
160
181
|
const primary = column.primary ? ' PRIMARY KEY' : '';
|
|
161
182
|
const unique = column.unique ? ' UNIQUE' : '';
|
|
183
|
+
const autoIncrement = column.autoIncrement ? ' GENERATED BY DEFAULT AS IDENTITY' : '';
|
|
162
184
|
const defaultVal = column.default !== undefined
|
|
163
185
|
? ` DEFAULT ${this.formatDefaultValue(column.default)}`
|
|
164
186
|
: '';
|
|
165
|
-
return `${name} ${type}${length}${primary}${unique}${nullable}${defaultVal}`;
|
|
187
|
+
return `${name} ${type}${length}${autoIncrement}${primary}${unique}${nullable}${defaultVal}`;
|
|
188
|
+
}
|
|
189
|
+
async reconcileDefinitions(definitions) {
|
|
190
|
+
const expected = new Map();
|
|
191
|
+
for (const [tableName, definition] of definitions) {
|
|
192
|
+
for (const [columnName, column] of Object.entries(definition)) {
|
|
193
|
+
if (column.type === 'bigint' || column.autoIncrement) {
|
|
194
|
+
expected.set(`${String(tableName)}\0${columnName}`, column);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (expected.size === 0)
|
|
199
|
+
return;
|
|
200
|
+
const tableNames = [...new Set([...expected.keys()].map((key) => key.slice(0, key.indexOf('\0'))))];
|
|
201
|
+
const rows = await this.query(`SELECT table_name, column_name, data_type, is_identity, column_default
|
|
202
|
+
FROM information_schema.columns
|
|
203
|
+
WHERE table_schema = current_schema() AND table_name = ANY($1::text[])`, [tableNames]);
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const definition = expected.get(`${row.table_name}\0${row.column_name}`);
|
|
206
|
+
if (!definition)
|
|
207
|
+
continue;
|
|
208
|
+
const table = this.quoteIdentifier(row.table_name);
|
|
209
|
+
const column = this.quoteIdentifier(row.column_name);
|
|
210
|
+
if (definition.type === 'bigint' && (row.data_type === 'smallint' || row.data_type === 'integer')) {
|
|
211
|
+
await this.query(`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE BIGINT USING ${column}::BIGINT`);
|
|
212
|
+
}
|
|
213
|
+
const hasSequenceDefault = row.column_default?.startsWith('nextval(') ?? false;
|
|
214
|
+
if (definition.autoIncrement && row.is_identity !== 'YES' && !hasSequenceDefault) {
|
|
215
|
+
await this.query(`ALTER TABLE ${table} ALTER COLUMN ${column} ADD GENERATED BY DEFAULT AS IDENTITY`);
|
|
216
|
+
const maximum = await this.query(`SELECT MAX(${column}) AS max_value FROM ${table}`);
|
|
217
|
+
const maximumValue = maximum[0]?.max_value;
|
|
218
|
+
const next = (maximumValue == null ? 1n : BigInt(maximumValue) + 1n).toString();
|
|
219
|
+
const relation = `"${row.table_name.replaceAll('"', '""')}"`;
|
|
220
|
+
await this.query('SELECT setval(pg_get_serial_sequence($1, $2), $3, false)', [relation, row.column_name, next]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
166
223
|
}
|
|
167
224
|
formatAlterTable(tableName, alterations) {
|
|
168
225
|
return `ALTER TABLE ${this.quoteIdentifier(String(tableName))} ${alterations.join(', ')}`;
|
|
@@ -189,6 +246,7 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
189
246
|
* 在连接池模式下,会获取一个专用连接用于事务
|
|
190
247
|
*/
|
|
191
248
|
async beginTransaction(options) {
|
|
249
|
+
const dialect = this;
|
|
192
250
|
if (this.usePool) {
|
|
193
251
|
// 从连接池获取一个连接用于事务
|
|
194
252
|
const client = await this.pool.connect();
|
|
@@ -217,7 +275,7 @@ export class PostgreSQLDialect extends Dialect {
|
|
|
217
275
|
},
|
|
218
276
|
async query(sql, params) {
|
|
219
277
|
const result = await client.query(sql, params);
|
|
220
|
-
return result
|
|
278
|
+
return dialect.normalizeResult(result);
|
|
221
279
|
}
|
|
222
280
|
};
|
|
223
281
|
}
|
package/lib/dialects/redis.js
CHANGED
|
@@ -436,7 +436,7 @@ export class RedisDialect extends Dialect {
|
|
|
436
436
|
throw new Error('EXPIRE 操作需要指定 key');
|
|
437
437
|
}
|
|
438
438
|
const key = `${keyPrefix}${query.key}`;
|
|
439
|
-
const ttl = params[0]
|
|
439
|
+
const ttl = query.ttl ?? params[0];
|
|
440
440
|
const result = await this.client.expire(key, ttl);
|
|
441
441
|
return [{ key: query.key, result }];
|
|
442
442
|
}
|
|
@@ -481,6 +481,7 @@ export class RedisDialect extends Dialect {
|
|
|
481
481
|
dataTypes: {
|
|
482
482
|
'string': 'String',
|
|
483
483
|
'integer': 'Integer',
|
|
484
|
+
'bigint': 'Integer',
|
|
484
485
|
'float': 'Float',
|
|
485
486
|
'boolean': 'Boolean',
|
|
486
487
|
'date': 'Timestamp',
|
package/lib/dialects/sqlite.js
CHANGED
|
@@ -154,6 +154,7 @@ export class SQLiteDialect extends Dialect {
|
|
|
154
154
|
const typeMap = {
|
|
155
155
|
'text': 'TEXT',
|
|
156
156
|
'integer': 'INTEGER',
|
|
157
|
+
'bigint': 'INTEGER',
|
|
157
158
|
'float': 'REAL',
|
|
158
159
|
'boolean': 'INTEGER',
|
|
159
160
|
'date': 'TEXT',
|
|
@@ -217,14 +218,16 @@ export class SQLiteDialect extends Dialect {
|
|
|
217
218
|
formatColumnDefinition(field, column) {
|
|
218
219
|
const name = this.quoteIdentifier(String(field));
|
|
219
220
|
const type = this.mapColumnType(column.type);
|
|
220
|
-
|
|
221
|
+
// SQLite only enables rowid auto-increment for the exact INTEGER PRIMARY KEY form.
|
|
222
|
+
const length = column.length && !column.autoIncrement ? `(${column.length})` : '';
|
|
221
223
|
const nullable = column.nullable === false ? ' NOT NULL' : '';
|
|
222
224
|
const primary = column.primary ? ' PRIMARY KEY' : '';
|
|
223
225
|
const unique = column.unique ? ' UNIQUE' : '';
|
|
226
|
+
const autoIncrement = column.autoIncrement ? ' AUTOINCREMENT' : '';
|
|
224
227
|
const defaultVal = column.default !== undefined
|
|
225
228
|
? ` DEFAULT ${this.formatDefaultValue(column.default)}`
|
|
226
229
|
: '';
|
|
227
|
-
return `${name} ${type}${length}${primary}${unique}${nullable}${defaultVal}`;
|
|
230
|
+
return `${name} ${type}${length}${primary}${autoIncrement}${unique}${nullable}${defaultVal}`;
|
|
228
231
|
}
|
|
229
232
|
formatAlterTable(tableName, alterations) {
|
|
230
233
|
return `ALTER TABLE ${this.quoteIdentifier(String(tableName))} ${alterations.join(', ')}`;
|
package/lib/migration.js
CHANGED
|
@@ -245,21 +245,9 @@ export class MigrationRunner {
|
|
|
245
245
|
const db = this.database;
|
|
246
246
|
return {
|
|
247
247
|
async createTable(tableName, columns) {
|
|
248
|
-
const columnDefs = Object.entries(columns)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def += ' PRIMARY KEY';
|
|
252
|
-
if (col.autoIncrement)
|
|
253
|
-
def += ' AUTOINCREMENT';
|
|
254
|
-
if (col.unique)
|
|
255
|
-
def += ' UNIQUE';
|
|
256
|
-
if (!col.nullable)
|
|
257
|
-
def += ' NOT NULL';
|
|
258
|
-
if (col.default !== undefined)
|
|
259
|
-
def += ` DEFAULT ${formatDefault(col.default)}`;
|
|
260
|
-
return def;
|
|
261
|
-
}).join(', ');
|
|
262
|
-
const sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
|
|
248
|
+
const columnDefs = Object.entries(columns)
|
|
249
|
+
.map(([name, column]) => db.dialect.formatColumnDefinition(name, column));
|
|
250
|
+
const sql = db.dialect.formatCreateTable(tableName, columnDefs);
|
|
263
251
|
await db.query(sql);
|
|
264
252
|
},
|
|
265
253
|
async dropTable(tableName) {
|
|
@@ -267,14 +255,8 @@ export class MigrationRunner {
|
|
|
267
255
|
await db.query(sql);
|
|
268
256
|
},
|
|
269
257
|
async addColumn(tableName, columnName, column) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def += ' UNIQUE';
|
|
273
|
-
if (!column.nullable)
|
|
274
|
-
def += ' NOT NULL';
|
|
275
|
-
if (column.default !== undefined)
|
|
276
|
-
def += ` DEFAULT ${formatDefault(column.default)}`;
|
|
277
|
-
const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${def}`;
|
|
258
|
+
const def = db.dialect.formatColumnDefinition(columnName, column);
|
|
259
|
+
const sql = `ALTER TABLE ${db.dialect.quoteIdentifier(tableName)} ADD COLUMN ${def}`;
|
|
278
260
|
await db.query(sql);
|
|
279
261
|
},
|
|
280
262
|
async dropColumn(tableName, columnName) {
|
|
@@ -437,33 +419,6 @@ export class MigrationRunner {
|
|
|
437
419
|
return { rolledBack, migrated };
|
|
438
420
|
}
|
|
439
421
|
}
|
|
440
|
-
/**
|
|
441
|
-
* 映射列类型到 SQL 类型
|
|
442
|
-
*/
|
|
443
|
-
function mapColumnType(type) {
|
|
444
|
-
const typeMap = {
|
|
445
|
-
'text': 'TEXT',
|
|
446
|
-
'integer': 'INTEGER',
|
|
447
|
-
'float': 'REAL',
|
|
448
|
-
'boolean': 'INTEGER',
|
|
449
|
-
'date': 'DATETIME',
|
|
450
|
-
'json': 'TEXT'
|
|
451
|
-
};
|
|
452
|
-
return typeMap[type] || type.toUpperCase();
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* 格式化默认值
|
|
456
|
-
*/
|
|
457
|
-
function formatDefault(value) {
|
|
458
|
-
if (value === null)
|
|
459
|
-
return 'NULL';
|
|
460
|
-
// 与各方言 escapeString 一致:单引号 doubling,避免默认值中的引号破坏 SQL
|
|
461
|
-
if (typeof value === 'string')
|
|
462
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
463
|
-
if (typeof value === 'boolean')
|
|
464
|
-
return value ? '1' : '0';
|
|
465
|
-
return String(value);
|
|
466
|
-
}
|
|
467
422
|
/**
|
|
468
423
|
* 创建迁移辅助函数
|
|
469
424
|
*
|
|
@@ -45,6 +45,7 @@ export class DocumentDatabase extends Database {
|
|
|
45
45
|
return {
|
|
46
46
|
query: {
|
|
47
47
|
collection: params.tableName,
|
|
48
|
+
operation: 'createCollection',
|
|
48
49
|
filter: {},
|
|
49
50
|
projection: {}
|
|
50
51
|
},
|
|
@@ -62,6 +63,7 @@ export class DocumentDatabase extends Database {
|
|
|
62
63
|
}
|
|
63
64
|
const query = {
|
|
64
65
|
collection: params.tableName,
|
|
66
|
+
operation: 'find',
|
|
65
67
|
filter
|
|
66
68
|
};
|
|
67
69
|
if (params.orderings) {
|
|
@@ -94,6 +96,7 @@ export class DocumentDatabase extends Database {
|
|
|
94
96
|
return {
|
|
95
97
|
query: {
|
|
96
98
|
collection: params.tableName,
|
|
99
|
+
operation: 'insertOne',
|
|
97
100
|
filter: {},
|
|
98
101
|
projection: {}
|
|
99
102
|
},
|
|
@@ -111,6 +114,7 @@ export class DocumentDatabase extends Database {
|
|
|
111
114
|
return {
|
|
112
115
|
query: {
|
|
113
116
|
collection: params.tableName,
|
|
117
|
+
operation: 'updateMany',
|
|
114
118
|
filter,
|
|
115
119
|
projection: {}
|
|
116
120
|
},
|
|
@@ -128,6 +132,7 @@ export class DocumentDatabase extends Database {
|
|
|
128
132
|
return {
|
|
129
133
|
query: {
|
|
130
134
|
collection: params.tableName,
|
|
135
|
+
operation: 'deleteMany',
|
|
131
136
|
filter,
|
|
132
137
|
projection: {}
|
|
133
138
|
},
|
|
@@ -138,14 +143,7 @@ export class DocumentDatabase extends Database {
|
|
|
138
143
|
* 构建修改集合查询
|
|
139
144
|
*/
|
|
140
145
|
buildAlterQuery(params) {
|
|
141
|
-
|
|
142
|
-
query: {
|
|
143
|
-
collection: params.tableName,
|
|
144
|
-
filter: {},
|
|
145
|
-
projection: {}
|
|
146
|
-
},
|
|
147
|
-
params: [params.alterations]
|
|
148
|
-
};
|
|
146
|
+
throw new TypeError(`Document schema alteration is not supported for collection ${String(params.tableName)}`);
|
|
149
147
|
}
|
|
150
148
|
/**
|
|
151
149
|
* 构建删除集合查询
|
|
@@ -154,6 +152,7 @@ export class DocumentDatabase extends Database {
|
|
|
154
152
|
return {
|
|
155
153
|
query: {
|
|
156
154
|
collection: params.tableName,
|
|
155
|
+
operation: 'dropCollection',
|
|
157
156
|
filter: {},
|
|
158
157
|
projection: {}
|
|
159
158
|
},
|
|
@@ -167,6 +166,7 @@ export class DocumentDatabase extends Database {
|
|
|
167
166
|
return {
|
|
168
167
|
query: {
|
|
169
168
|
collection: params.tableName,
|
|
169
|
+
operation: 'dropIndex',
|
|
170
170
|
filter: {},
|
|
171
171
|
projection: {}
|
|
172
172
|
},
|
|
@@ -31,13 +31,7 @@ export class KeyValueModel extends Model {
|
|
|
31
31
|
if (results.length === 0) {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
// 检查是否过期
|
|
36
|
-
if (row.expires_at && Date.now() > row.expires_at) {
|
|
37
|
-
await this.deleteByKey(key);
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return JSON.parse(row.value);
|
|
34
|
+
return results[0];
|
|
41
35
|
}
|
|
42
36
|
/**
|
|
43
37
|
* 删除键
|
|
@@ -48,7 +42,7 @@ export class KeyValueModel extends Model {
|
|
|
48
42
|
bucket: this.name,
|
|
49
43
|
key,
|
|
50
44
|
}, [key]);
|
|
51
|
-
return result
|
|
45
|
+
return result[0]?.deleted === true;
|
|
52
46
|
}
|
|
53
47
|
/**
|
|
54
48
|
* 检查键是否存在
|
|
@@ -59,7 +53,7 @@ export class KeyValueModel extends Model {
|
|
|
59
53
|
bucket: this.name,
|
|
60
54
|
key,
|
|
61
55
|
}, [key, Date.now()]);
|
|
62
|
-
return results
|
|
56
|
+
return results[0] === true;
|
|
63
57
|
}
|
|
64
58
|
/**
|
|
65
59
|
* 获取所有键
|
|
@@ -69,7 +63,7 @@ export class KeyValueModel extends Model {
|
|
|
69
63
|
operation: 'keys',
|
|
70
64
|
bucket: this.name,
|
|
71
65
|
}, [Date.now()]);
|
|
72
|
-
return results
|
|
66
|
+
return results;
|
|
73
67
|
}
|
|
74
68
|
/**
|
|
75
69
|
* 获取所有值
|
|
@@ -79,7 +73,7 @@ export class KeyValueModel extends Model {
|
|
|
79
73
|
operation: 'values',
|
|
80
74
|
bucket: this.name,
|
|
81
75
|
}, [Date.now()]);
|
|
82
|
-
return results
|
|
76
|
+
return results;
|
|
83
77
|
}
|
|
84
78
|
/**
|
|
85
79
|
* 获取所有键值对
|
|
@@ -89,7 +83,7 @@ export class KeyValueModel extends Model {
|
|
|
89
83
|
operation: 'entries',
|
|
90
84
|
bucket: this.name,
|
|
91
85
|
}, [Date.now()]);
|
|
92
|
-
return results
|
|
86
|
+
return results;
|
|
93
87
|
}
|
|
94
88
|
/**
|
|
95
89
|
* 清空桶
|
|
@@ -108,35 +102,27 @@ export class KeyValueModel extends Model {
|
|
|
108
102
|
operation: 'size',
|
|
109
103
|
bucket: this.name,
|
|
110
104
|
}, [Date.now()]);
|
|
111
|
-
return results[0]
|
|
105
|
+
return results[0] ?? 0;
|
|
112
106
|
}
|
|
113
107
|
/**
|
|
114
108
|
* 批量设置
|
|
115
109
|
*/
|
|
116
110
|
async setMany(entries, ttl) {
|
|
117
|
-
const expiresAt = ttl ? Date.now() + ttl * 1000 : null;
|
|
118
111
|
for (const [key, value] of entries) {
|
|
119
|
-
await this.
|
|
120
|
-
operation: 'set',
|
|
121
|
-
bucket: this.name,
|
|
122
|
-
key,
|
|
123
|
-
value,
|
|
124
|
-
ttl,
|
|
125
|
-
}, [key, JSON.stringify(value), expiresAt]);
|
|
112
|
+
await this.set(key, value, ttl);
|
|
126
113
|
}
|
|
127
114
|
}
|
|
128
115
|
/**
|
|
129
116
|
* 设置过期时间
|
|
130
117
|
*/
|
|
131
118
|
async expire(key, ttl) {
|
|
132
|
-
const expiresAt = Date.now() + ttl * 1000;
|
|
133
119
|
const result = await this.dialect.query({
|
|
134
120
|
operation: 'expire',
|
|
135
121
|
bucket: this.name,
|
|
136
122
|
key,
|
|
137
123
|
ttl,
|
|
138
|
-
}, [
|
|
139
|
-
return result
|
|
124
|
+
}, []);
|
|
125
|
+
return Boolean(result[0]?.result);
|
|
140
126
|
}
|
|
141
127
|
/**
|
|
142
128
|
* 获取剩余过期时间(秒)
|
|
@@ -150,12 +136,7 @@ export class KeyValueModel extends Model {
|
|
|
150
136
|
if (results.length === 0) {
|
|
151
137
|
return null;
|
|
152
138
|
}
|
|
153
|
-
|
|
154
|
-
if (!expiresAt) {
|
|
155
|
-
return -1; // 永不过期
|
|
156
|
-
}
|
|
157
|
-
const remaining = Math.ceil((expiresAt - Date.now()) / 1000);
|
|
158
|
-
return remaining > 0 ? remaining : 0;
|
|
139
|
+
return results[0];
|
|
159
140
|
}
|
|
160
141
|
/**
|
|
161
142
|
* 移除过期时间
|
|
@@ -166,7 +147,7 @@ export class KeyValueModel extends Model {
|
|
|
166
147
|
bucket: this.name,
|
|
167
148
|
key,
|
|
168
149
|
}, [key]);
|
|
169
|
-
return result
|
|
150
|
+
return Boolean(result[0]?.result);
|
|
170
151
|
}
|
|
171
152
|
/**
|
|
172
153
|
* 清理过期键
|
|
@@ -176,20 +157,18 @@ export class KeyValueModel extends Model {
|
|
|
176
157
|
operation: 'cleanup',
|
|
177
158
|
bucket: this.name,
|
|
178
159
|
}, [Date.now()]);
|
|
179
|
-
return result
|
|
160
|
+
return result[0]?.cleaned ?? 0;
|
|
180
161
|
}
|
|
181
162
|
/**
|
|
182
163
|
* 获取键的模式匹配
|
|
183
164
|
*/
|
|
184
165
|
async keysByPattern(pattern) {
|
|
185
|
-
// 简单的通配符匹配,将 * 转换为 SQL 的 %
|
|
186
|
-
const sqlPattern = pattern.replace(/\*/g, '%');
|
|
187
166
|
const results = await this.dialect.query({
|
|
188
167
|
operation: 'keysByPattern',
|
|
189
168
|
bucket: this.name,
|
|
190
|
-
pattern
|
|
191
|
-
}, [
|
|
192
|
-
return results
|
|
169
|
+
pattern,
|
|
170
|
+
}, []);
|
|
171
|
+
return results;
|
|
193
172
|
}
|
|
194
173
|
/**
|
|
195
174
|
* 原子操作:如果不存在则设置
|
|
@@ -48,7 +48,7 @@ export declare class RelatedDatabase<D = any, S extends Record<string, object> =
|
|
|
48
48
|
* @param condition 条件对象
|
|
49
49
|
* @param tablePrefix 表名前缀(用于 JOIN 查询)
|
|
50
50
|
*/
|
|
51
|
-
protected parseCondition<T extends object>(condition: Condition<T>, tablePrefix?: string): [string, any[]];
|
|
51
|
+
protected parseCondition<T extends object>(condition: Condition<T>, tablePrefix?: string, parameterOffset?: number): [string, any[]];
|
|
52
52
|
/**
|
|
53
53
|
* 获取模型
|
|
54
54
|
* @param name 模型名称
|
|
@@ -40,12 +40,14 @@ export class RelatedDatabase extends Database {
|
|
|
40
40
|
return this;
|
|
41
41
|
}
|
|
42
42
|
async initialize() {
|
|
43
|
-
//
|
|
43
|
+
// 单连接方言(PostgreSQL Client 等)不允许同一连接并发 query。
|
|
44
|
+
// 初始化只发生在启动阶段,按定义顺序建表也让失败位置保持确定。
|
|
44
45
|
const tableEntries = Array.from(this.definitions.entries());
|
|
45
|
-
|
|
46
|
+
for (const [tableName, definition] of tableEntries) {
|
|
46
47
|
await this.create(tableName, definition);
|
|
47
|
-
}
|
|
48
|
-
|
|
48
|
+
}
|
|
49
|
+
await this.dialect.reconcileDefinitions(this.definitions);
|
|
50
|
+
// 创建完成后,统一设置 models。
|
|
49
51
|
for (const [tableName, definition] of tableEntries) {
|
|
50
52
|
this.models.set(tableName, new RelatedModel(this, tableName, definition));
|
|
51
53
|
}
|
|
@@ -215,7 +217,7 @@ export class RelatedDatabase extends Database {
|
|
|
215
217
|
const queryParams = [...Object.values(params.update)];
|
|
216
218
|
// WHERE clause
|
|
217
219
|
if (params.conditions) {
|
|
218
|
-
const [condition, conditionParams] = this.parseCondition(params.conditions);
|
|
220
|
+
const [condition, conditionParams] = this.parseCondition(params.conditions, undefined, queryParams.length);
|
|
219
221
|
if (condition) {
|
|
220
222
|
query += ` WHERE ${condition}`;
|
|
221
223
|
queryParams.push(...conditionParams);
|
|
@@ -297,7 +299,7 @@ export class RelatedDatabase extends Database {
|
|
|
297
299
|
}
|
|
298
300
|
// HAVING clause
|
|
299
301
|
if (params.havingConditions && Object.keys(params.havingConditions).length) {
|
|
300
|
-
const [havingCondition, havingParams] = this.parseCondition(params.havingConditions);
|
|
302
|
+
const [havingCondition, havingParams] = this.parseCondition(params.havingConditions, undefined, queryParams.length);
|
|
301
303
|
if (havingCondition) {
|
|
302
304
|
query += ` HAVING ${havingCondition}`;
|
|
303
305
|
queryParams.push(...havingParams);
|
|
@@ -309,16 +311,7 @@ export class RelatedDatabase extends Database {
|
|
|
309
311
|
// Helper Methods
|
|
310
312
|
// ========================================================================
|
|
311
313
|
formatColumnDefinition(field, column) {
|
|
312
|
-
|
|
313
|
-
const type = this.dialect.mapColumnType(column.type);
|
|
314
|
-
const length = column.length ? `(${column.length})` : '';
|
|
315
|
-
const nullable = column.nullable === false ? ' NOT NULL' : '';
|
|
316
|
-
const primary = column.primary ? ' PRIMARY KEY' : '';
|
|
317
|
-
const unique = column.unique ? ' UNIQUE' : '';
|
|
318
|
-
const defaultVal = column.default !== undefined
|
|
319
|
-
? ` DEFAULT ${this.dialect.formatDefaultValue(column.default)}`
|
|
320
|
-
: '';
|
|
321
|
-
return `${name} ${type}${length}${primary}${unique}${nullable}${defaultVal}`;
|
|
314
|
+
return this.dialect.formatColumnDefinition(field, column);
|
|
322
315
|
}
|
|
323
316
|
formatAlteration(field, alteration) {
|
|
324
317
|
const name = this.dialect.quoteIdentifier(field);
|
|
@@ -354,7 +347,7 @@ export class RelatedDatabase extends Database {
|
|
|
354
347
|
* @param condition 条件对象
|
|
355
348
|
* @param tablePrefix 表名前缀(用于 JOIN 查询)
|
|
356
349
|
*/
|
|
357
|
-
parseCondition(condition, tablePrefix) {
|
|
350
|
+
parseCondition(condition, tablePrefix, parameterOffset = 0) {
|
|
358
351
|
const clauses = [];
|
|
359
352
|
const params = [];
|
|
360
353
|
// 辅助函数:生成带前缀的字段名
|
|
@@ -368,7 +361,7 @@ export class RelatedDatabase extends Database {
|
|
|
368
361
|
if (key === '$and' && Array.isArray(condition.$and)) {
|
|
369
362
|
const subClauses = [];
|
|
370
363
|
for (const subCondition of condition.$and) {
|
|
371
|
-
const [subClause, subParams] = this.parseCondition(subCondition, tablePrefix);
|
|
364
|
+
const [subClause, subParams] = this.parseCondition(subCondition, tablePrefix, parameterOffset + params.length);
|
|
372
365
|
if (subClause) {
|
|
373
366
|
subClauses.push(`(${subClause})`);
|
|
374
367
|
params.push(...subParams);
|
|
@@ -381,7 +374,7 @@ export class RelatedDatabase extends Database {
|
|
|
381
374
|
else if (key === '$or' && Array.isArray(condition.$or)) {
|
|
382
375
|
const subClauses = [];
|
|
383
376
|
for (const subCondition of condition.$or) {
|
|
384
|
-
const [subClause, subParams] = this.parseCondition(subCondition, tablePrefix);
|
|
377
|
+
const [subClause, subParams] = this.parseCondition(subCondition, tablePrefix, parameterOffset + params.length);
|
|
385
378
|
if (subClause) {
|
|
386
379
|
subClauses.push(`(${subClause})`);
|
|
387
380
|
params.push(...subParams);
|
|
@@ -392,7 +385,7 @@ export class RelatedDatabase extends Database {
|
|
|
392
385
|
}
|
|
393
386
|
}
|
|
394
387
|
else if (key === '$not' && condition.$not) {
|
|
395
|
-
const [subClause, subParams] = this.parseCondition(condition.$not, tablePrefix);
|
|
388
|
+
const [subClause, subParams] = this.parseCondition(condition.$not, tablePrefix, parameterOffset + params.length);
|
|
396
389
|
if (subClause) {
|
|
397
390
|
clauses.push(`NOT (${subClause})`);
|
|
398
391
|
params.push(...subParams);
|
|
@@ -409,7 +402,7 @@ export class RelatedDatabase extends Database {
|
|
|
409
402
|
if (isPlainObject) {
|
|
410
403
|
for (const op in value) {
|
|
411
404
|
const quotedKey = formatField(key);
|
|
412
|
-
const placeholder = this.dialect.getParameterPlaceholder(params.length);
|
|
405
|
+
const placeholder = this.dialect.getParameterPlaceholder(parameterOffset + params.length);
|
|
413
406
|
switch (op) {
|
|
414
407
|
case '$eq':
|
|
415
408
|
if (value[op] === null) {
|
|
@@ -453,7 +446,7 @@ export class RelatedDatabase extends Database {
|
|
|
453
446
|
params.push(...subquery.params);
|
|
454
447
|
}
|
|
455
448
|
else if (Array.isArray(value[op]) && value[op].length) {
|
|
456
|
-
const placeholders = value[op].map((_, i) => this.dialect.getParameterPlaceholder(params.length + i));
|
|
449
|
+
const placeholders = value[op].map((_, i) => this.dialect.getParameterPlaceholder(parameterOffset + params.length + i));
|
|
457
450
|
clauses.push(`${quotedKey} IN (${placeholders.join(', ')})`);
|
|
458
451
|
params.push(...value[op]);
|
|
459
452
|
}
|
|
@@ -469,7 +462,7 @@ export class RelatedDatabase extends Database {
|
|
|
469
462
|
params.push(...subquery.params);
|
|
470
463
|
}
|
|
471
464
|
else if (Array.isArray(value[op]) && value[op].length) {
|
|
472
|
-
const placeholders = value[op].map((_, i) => this.dialect.getParameterPlaceholder(params.length + i));
|
|
465
|
+
const placeholders = value[op].map((_, i) => this.dialect.getParameterPlaceholder(parameterOffset + params.length + i));
|
|
473
466
|
clauses.push(`${quotedKey} NOT IN (${placeholders.join(', ')})`);
|
|
474
467
|
params.push(...value[op]);
|
|
475
468
|
}
|
|
@@ -494,7 +487,7 @@ export class RelatedDatabase extends Database {
|
|
|
494
487
|
clauses.push(`${quotedKey} IS NULL`);
|
|
495
488
|
}
|
|
496
489
|
else {
|
|
497
|
-
const placeholder = this.dialect.getParameterPlaceholder(params.length);
|
|
490
|
+
const placeholder = this.dialect.getParameterPlaceholder(parameterOffset + params.length);
|
|
498
491
|
clauses.push(`${quotedKey} = ${placeholder}`);
|
|
499
492
|
params.push(value);
|
|
500
493
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface AggregateField<T extends object> {
|
|
|
5
5
|
field: keyof T | '*';
|
|
6
6
|
alias?: string;
|
|
7
7
|
}
|
|
8
|
-
export type ColumnType = "text" | "integer" | "float" | "boolean" | "date" | "json";
|
|
8
|
+
export type ColumnType = "text" | "integer" | "bigint" | "float" | "boolean" | "date" | "json";
|
|
9
9
|
export interface Column<T = any> {
|
|
10
10
|
type: ColumnType;
|
|
11
11
|
nullable?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/database",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Universal database abstraction layer for Zhin.js - supports SQLite, MySQL, PostgreSQL, MongoDB, Redis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"mongodb": "^7.5.0",
|
|
48
48
|
"mysql2": "^3.23.2",
|
|
49
49
|
"pg": "^8.22.0",
|
|
50
|
-
"redis": "
|
|
50
|
+
"redis": "^6.1.0",
|
|
51
51
|
"typescript": "^6.0.3"
|
|
52
52
|
},
|
|
53
53
|
"repository": {
|