@zhin.js/database 1.1.1 → 1.1.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/README.md CHANGED
@@ -108,6 +108,22 @@ await db.start();
108
108
 
109
109
  ### MySQL Example
110
110
 
111
+ 框架初始化验证基线为 **MySQL 8.4 LTS**,驱动为 `mysql2` 3.x。项目配置中的
112
+ `database.dialect: mysql` 用于选择方言,由 CLI 移除后再把连接参数传给驱动。
113
+ 不要为了消除驱动警告而删除项目配置中的 `dialect`。
114
+
115
+ MySQL 方言将 `text` 的非空默认值和 `json` 默认值写成表达式,例如
116
+ `TEXT DEFAULT ('')`;普通 `TEXT DEFAULT ''` 会被 MySQL 拒绝。
117
+ 声明了 `length` 的 `text` 使用 `VARCHAR(length)`;未声明长度的文本主键或唯一键
118
+ 使用 `VARCHAR(768)`,以完整值建立索引;显式声明的索引文本长度超过 768 时直接报错。文本主键和唯一键使用 `utf8mb4_0900_bin`,保留大小写及末尾空格的区别。其余文本仍为 `TEXT`。
119
+ 新表显式使用 InnoDB `ROW_FORMAT=DYNAMIC`。使用 4KB / 8KB InnoDB 页时,启动会先校验文本索引长度,并提示配置更大的页或显式声明更短的键长度。
120
+ 键值应在该长度内;需要更短的业务边界时显式声明 `length`。这里没有使用文本前缀唯一索引,
121
+ 避免将前缀相同的不同键误判为重复。
122
+
123
+ 如果启动报 `message_key can't have a default value`,请升级包含此修复的
124
+ `@zhin.js/database` 与 `@zhin.js/cli` 后重启。该错误来自建表 SQL,无需降级
125
+ MySQL 或 mysql2,也无需删除已有数据库。修复针对新建表,不会缩窄既有文本列。
126
+
111
127
  ```typescript
112
128
  const db = Registry.create('mysql', {
113
129
  host: 'localhost',
@@ -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
  */
@@ -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
  // ============================================================================
@@ -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
  }
@@ -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> {
@@ -49,7 +49,8 @@ export class MongoDBDialect extends Dialect {
49
49
  try {
50
50
  // 动态导入 mongodb 客户端
51
51
  const { MongoClient } = await import('mongodb');
52
- this.client = new MongoClient(this.config.url, this.config);
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',
@@ -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;
@@ -57,4 +64,5 @@ export declare class MySQLDialect<S extends Record<string, object> = Record<stri
57
64
  }
58
65
  export declare class MySQL<S extends Record<string, object> = Record<string, object>> extends RelatedDatabase<MySQLDialectConfig, S> {
59
66
  constructor(config: MySQLDialectConfig, definitions?: Database.DefinitionObj<S>);
67
+ protected initialize(): Promise<void>;
60
68
  }
@@ -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
- ...this.config,
26
+ ...connectionOptions,
24
27
  waitForConnections: true,
25
- connectionLimit: this.config.pool?.max ?? 10,
28
+ connectionLimit: pool?.max ?? 10,
26
29
  queueLimit: 0,
27
- idleTimeout: this.config.pool?.idleTimeoutMillis ?? 60000,
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',
@@ -149,20 +187,69 @@ export class MySQLDialect extends Dialect {
149
187
  return `LIMIT ${offset}, ${limit}`;
150
188
  }
151
189
  formatCreateTable(tableName, columns) {
152
- return `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(String(tableName))} (${columns.join(', ')}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
190
+ return `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(String(tableName))} (${columns.join(', ')}) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4`;
153
191
  }
154
192
  formatColumnDefinition(field, column) {
155
193
  const name = this.quoteIdentifier(String(field));
156
- const type = this.mapColumnType(column.type);
157
- const length = column.length ? `(${column.length})` : '';
194
+ // TEXT cannot be indexed in full. Bounded keys retain full-value uniqueness
195
+ // within InnoDB's 3072-byte index limit under utf8mb4 (4 bytes per character).
196
+ const boundedText = column.type.toLowerCase() === 'text'
197
+ && (column.length !== undefined || column.primary || column.unique);
198
+ const indexedText = boundedText && (column.primary || column.unique);
199
+ if (boundedText && (!Number.isInteger(column.length ?? 768) || (column.length ?? 768) < 1)) {
200
+ throw new RangeError(`MySQL text column ${field} requires a positive integer length`);
201
+ }
202
+ if (indexedText && (column.length ?? 768) > 768) {
203
+ throw new RangeError(`MySQL indexed text column ${field} exceeds the utf8mb4 index limit of 768 characters`);
204
+ }
205
+ const collation = indexedText ? ' CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin' : '';
206
+ const type = boundedText ? 'VARCHAR' : this.mapColumnType(column.type);
207
+ const length = boundedText ? `(${column.length ?? 768})` : column.length ? `(${column.length})` : '';
158
208
  const nullable = column.nullable === false ? ' NOT NULL' : '';
159
209
  const primary = column.primary ? ' PRIMARY KEY' : '';
160
210
  const unique = column.unique ? ' UNIQUE' : '';
161
211
  const autoIncrement = column.autoIncrement ? ' AUTO_INCREMENT' : '';
162
- const defaultVal = column.default !== undefined
163
- ? ` DEFAULT ${this.formatDefaultValue(column.default)}`
164
- : '';
165
- return `${name} ${type}${length}${primary}${unique}${autoIncrement}${nullable}${defaultVal}`;
212
+ let defaultVal = '';
213
+ if (column.default !== undefined) {
214
+ const value = this.formatDefaultValue(column.default);
215
+ // MySQL 8.0.13+ requires even literal TEXT/JSON defaults to be expressions.
216
+ const expression = (type === 'TEXT' || type === 'JSON') && column.default !== null;
217
+ defaultVal = ` DEFAULT ${expression ? `(${value})` : value}`;
218
+ }
219
+ return `${name} ${type}${length}${collation}${primary}${unique}${autoIncrement}${nullable}${defaultVal}`;
220
+ }
221
+ async reconcileDefinitions(definitions) {
222
+ const expected = new Map();
223
+ for (const [tableName, definition] of definitions) {
224
+ for (const [columnName, column] of Object.entries(definition)) {
225
+ if (column.type === 'bigint' || column.autoIncrement) {
226
+ expected.set(`${String(tableName)}\0${columnName}`, column);
227
+ }
228
+ }
229
+ }
230
+ if (expected.size === 0)
231
+ return;
232
+ const tableNames = [...new Set([...expected.keys()].map((key) => key.slice(0, key.indexOf('\0'))))];
233
+ const placeholders = tableNames.map(() => '?').join(', ');
234
+ const rows = await this.query(`SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type, EXTRA AS extra
235
+ FROM INFORMATION_SCHEMA.COLUMNS
236
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${placeholders})`, tableNames);
237
+ const narrowTypes = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer']);
238
+ for (const row of rows) {
239
+ const column = expected.get(`${row.table_name}\0${row.column_name}`);
240
+ if (!column)
241
+ continue;
242
+ const needsBigint = column.type === 'bigint' && narrowTypes.has(row.data_type.toLowerCase());
243
+ const needsAutoIncrement = Boolean(column.autoIncrement) && !row.extra.toLowerCase().includes('auto_increment');
244
+ if (!needsBigint && !needsAutoIncrement)
245
+ continue;
246
+ const definition = this.formatColumnDefinition(row.column_name, {
247
+ ...column,
248
+ primary: false,
249
+ unique: false,
250
+ });
251
+ await this.query(`ALTER TABLE ${this.quoteIdentifier(row.table_name)} MODIFY COLUMN ${definition}`);
252
+ }
166
253
  }
167
254
  formatAlterTable(tableName, alterations) {
168
255
  return `ALTER TABLE ${this.quoteIdentifier(String(tableName))} ${alterations.join(', ')}`;
@@ -189,19 +276,20 @@ export class MySQLDialect extends Dialect {
189
276
  * 在连接池模式下,会获取一个专用连接用于事务
190
277
  */
191
278
  async beginTransaction(options) {
279
+ const dialect = this;
192
280
  if (this.usePool) {
193
281
  // 从连接池获取一个连接用于事务
194
282
  const connection = await this.pool.getConnection();
195
283
  // 设置隔离级别
196
284
  if (options?.isolationLevel) {
197
- await connection.execute(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
285
+ await connection.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
198
286
  }
199
287
  // 开始事务
200
- await connection.execute('START TRANSACTION');
288
+ await connection.beginTransaction();
201
289
  return {
202
290
  async commit() {
203
291
  try {
204
- await connection.execute('COMMIT');
292
+ await connection.commit();
205
293
  }
206
294
  finally {
207
295
  connection.release(); // 归还连接到池
@@ -209,36 +297,36 @@ export class MySQLDialect extends Dialect {
209
297
  },
210
298
  async rollback() {
211
299
  try {
212
- await connection.execute('ROLLBACK');
300
+ await connection.rollback();
213
301
  }
214
302
  finally {
215
303
  connection.release(); // 归还连接到池
216
304
  }
217
305
  },
218
306
  async query(sql, params) {
219
- const [rows] = await connection.execute(sql, params);
220
- return rows;
307
+ const [rows, fields] = await connection.execute(sql, params);
308
+ return dialect.processQueryResults(rows, fields);
221
309
  }
222
310
  };
223
311
  }
224
312
  else {
225
- // 单连接模式
226
- const dialect = this;
227
313
  // 设置隔离级别
228
314
  if (options?.isolationLevel) {
229
- await this.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
315
+ await this.connection.query(`SET TRANSACTION ISOLATION LEVEL ${this.formatIsolationLevel(options.isolationLevel)}`);
230
316
  }
231
317
  // 开始事务
232
- await this.query('START TRANSACTION');
318
+ await this.connection.beginTransaction();
319
+ const connection = this.connection;
233
320
  return {
234
321
  async commit() {
235
- await dialect.query('COMMIT');
322
+ await connection.commit();
236
323
  },
237
324
  async rollback() {
238
- await dialect.query('ROLLBACK');
325
+ await connection.rollback();
239
326
  },
240
327
  async query(sql, params) {
241
- return dialect.query(sql, params);
328
+ const [rows, fields] = await connection.execute(sql, params);
329
+ return dialect.processQueryResults(rows, fields);
242
330
  }
243
331
  };
244
332
  }
@@ -248,5 +336,20 @@ export class MySQL extends RelatedDatabase {
248
336
  constructor(config, definitions) {
249
337
  super(new MySQLDialect(config), definitions);
250
338
  }
339
+ async initialize() {
340
+ const indexedText = [...this.definitions.values()].flatMap(definition => Object.entries(definition).filter(([, column]) => column.type.toLowerCase() === 'text' && (column.primary || column.unique)));
341
+ if (indexedText.length) {
342
+ const [settings] = await this.dialect.query('SELECT @@innodb_page_size AS page_size');
343
+ const maxLength = Math.min(768, Number(settings?.page_size) * 3 / 64);
344
+ if (!Number.isFinite(maxLength) || maxLength < 1)
345
+ throw new Error('Cannot determine MySQL InnoDB text index limit');
346
+ for (const [name, column] of indexedText) {
347
+ if ((column.length ?? 768) > maxLength) {
348
+ throw new RangeError(`MySQL indexed text column ${name} requires ${column.length ?? 768} characters, but innodb_page_size=${settings.page_size} allows ${maxLength}; use a 16KB-or-larger InnoDB page size or declare a smaller key length`);
349
+ }
350
+ }
351
+ }
352
+ await super.initialize();
353
+ }
251
354
  }
252
355
  Registry.register('mysql', MySQL);
@@ -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;
@@ -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
- ...this.config,
24
- max: this.config.pool?.max ?? 10,
25
- min: this.config.pool?.min ?? 2,
26
- idleTimeoutMillis: this.config.pool?.idleTimeoutMillis ?? 30000,
27
- connectionTimeoutMillis: this.config.pool?.acquireTimeoutMillis ?? 10000,
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.rows;
69
+ return this.normalizeResult(result);
69
70
  }
70
71
  else {
71
72
  const result = await this.connection.query(sql, params);
72
- return result.rows;
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.rows;
278
+ return dialect.normalizeResult(result);
221
279
  }
222
280
  };
223
281
  }
@@ -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] || query.ttl;
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',
@@ -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
- const length = column.length ? `(${column.length})` : '';
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).map(([name, col]) => {
249
- let def = `"${name}" ${mapColumnType(col.type)}`;
250
- if (col.primary)
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
- let def = `"${columnName}" ${mapColumnType(column.type)}`;
271
- if (column.unique)
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
- return {
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
  },
@@ -36,7 +36,7 @@ export class DocumentModel extends Model {
36
36
  * 根据ID查找文档
37
37
  */
38
38
  async selectById(id) {
39
- return this.select('_id').where({
39
+ return this.select().where({
40
40
  _id: id,
41
41
  }).limit(1).then((results) => results[0] || null);
42
42
  }
@@ -31,13 +31,7 @@ export class KeyValueModel extends Model {
31
31
  if (results.length === 0) {
32
32
  return null;
33
33
  }
34
- const row = results[0];
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.affectedRows > 0;
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.length > 0;
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.map((row) => row.key);
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.map((row) => JSON.parse(row.value));
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.map((row) => [row.key, JSON.parse(row.value)]);
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]?.count || 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.dialect.query({
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
- }, [expiresAt, key]);
139
- return result.affectedRows > 0;
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
- const expiresAt = results[0].expires_at;
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.affectedRows > 0;
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.affectedRows;
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: sqlPattern,
191
- }, [sqlPattern, Date.now()]);
192
- return results.map((row) => row.key);
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
- await Promise.all(tableEntries.map(async ([tableName, definition]) => {
46
+ for (const [tableName, definition] of tableEntries) {
46
47
  await this.create(tableName, definition);
47
- }));
48
- // 创建完成后,统一设置 models(避免并发竞争)
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
- const name = this.dialect.quoteIdentifier(String(field));
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.1",
3
+ "version": "1.1.3",
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": "latest",
50
+ "redis": "^6.1.0",
51
51
  "typescript": "^6.0.3"
52
52
  },
53
53
  "repository": {