oak-db 3.3.12 → 3.3.13
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/MySQL/store.d.ts +2 -15
- package/lib/MySQL/store.js +1 -1
- package/lib/MySQL/translator.js +4 -2
- package/lib/PostgreSQL/connector.d.ts +0 -4
- package/lib/PostgreSQL/connector.js +0 -10
- package/lib/PostgreSQL/store.d.ts +13 -1
- package/lib/PostgreSQL/store.js +207 -9
- package/lib/PostgreSQL/translator.d.ts +21 -1
- package/lib/PostgreSQL/translator.js +397 -8
- package/lib/sqlTranslator.js +6 -4
- package/lib/types/dbStore.d.ts +26 -2
- package/package.json +2 -2
package/lib/MySQL/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult
|
|
1
|
+
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult } from 'oak-domain/lib/types';
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
3
|
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
|
4
4
|
import { MySQLConfiguration } from './types/Configuration';
|
|
@@ -7,7 +7,7 @@ import { MySqlTranslator, MySqlSelectOption, MysqlOperateOption } from './transl
|
|
|
7
7
|
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
|
8
8
|
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
|
9
9
|
import { CreateEntityOption } from '../types/Translator';
|
|
10
|
-
import { DbStore } from '../types/dbStore';
|
|
10
|
+
import { DbStore, Plan } from '../types/dbStore';
|
|
11
11
|
export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
|
12
12
|
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
|
|
13
13
|
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
|
|
@@ -48,16 +48,3 @@ export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt exte
|
|
|
48
48
|
*/
|
|
49
49
|
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
|
50
50
|
}
|
|
51
|
-
type Plan = {
|
|
52
|
-
newTables: Record<string, {
|
|
53
|
-
attributes: Record<string, Attribute>;
|
|
54
|
-
}>;
|
|
55
|
-
newIndexes: Record<string, Index<any>[]>;
|
|
56
|
-
updatedTables: Record<string, {
|
|
57
|
-
attributes: Record<string, Attribute & {
|
|
58
|
-
isNew: boolean;
|
|
59
|
-
}>;
|
|
60
|
-
}>;
|
|
61
|
-
updatedIndexes: Record<string, Index<any>[]>;
|
|
62
|
-
};
|
|
63
|
-
export {};
|
package/lib/MySQL/store.js
CHANGED
|
@@ -354,7 +354,7 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
|
|
|
354
354
|
};
|
|
355
355
|
for (const attr in attributesNew) {
|
|
356
356
|
if (attributes[attr]) {
|
|
357
|
-
// 因为反向无法复原原来定义的attribute类型,这里就比较两次创建的sql
|
|
357
|
+
// 因为反向无法复原原来定义的attribute类型,这里就比较两次创建的sql是不是一致,不是太好的设计。
|
|
358
358
|
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
|
359
359
|
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
|
360
360
|
if (!this.translator.compareSql(sql1, sql2)) {
|
package/lib/MySQL/translator.js
CHANGED
|
@@ -872,8 +872,10 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
872
872
|
const refId = (expr)['#refId'];
|
|
873
873
|
const refAttr = (expr)['#refAttr'];
|
|
874
874
|
(0, assert_1.default)(refDict[refId]);
|
|
875
|
-
const
|
|
876
|
-
|
|
875
|
+
const [refAlias, refEntity] = refDict[refId];
|
|
876
|
+
const attrText = `\`${refAlias}\`.\`${refAttr}\``;
|
|
877
|
+
// 这里必须使用refEntity,否则在filter深层嵌套节点表达式时会出现entity不对应
|
|
878
|
+
result = this.translateAttrInExpression(refEntity, (expr)['#refAttr'], attrText);
|
|
877
879
|
}
|
|
878
880
|
else {
|
|
879
881
|
(0, assert_1.default)(k.length === 1);
|
|
@@ -16,10 +16,6 @@ export declare class PostgreSQLConnector {
|
|
|
16
16
|
exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]>;
|
|
17
17
|
commitTransaction(txn: string): Promise<void>;
|
|
18
18
|
rollbackTransaction(txn: string): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* 执行多条 SQL 语句(用于初始化等场景)
|
|
21
|
-
*/
|
|
22
|
-
execBatch(sqls: string[], txn?: string): Promise<void>;
|
|
23
19
|
/**
|
|
24
20
|
* 获取连接池状态
|
|
25
21
|
*/
|
|
@@ -133,16 +133,6 @@ class PostgreSQLConnector {
|
|
|
133
133
|
connection.release();
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
/**
|
|
137
|
-
* 执行多条 SQL 语句(用于初始化等场景)
|
|
138
|
-
*/
|
|
139
|
-
async execBatch(sqls, txn) {
|
|
140
|
-
for (const sql of sqls) {
|
|
141
|
-
if (sql.trim()) {
|
|
142
|
-
await this.exec(sql, txn);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
136
|
/**
|
|
147
137
|
* 获取连接池状态
|
|
148
138
|
*/
|
|
@@ -7,7 +7,7 @@ import { PostgreSQLTranslator, PostgreSQLSelectOption, PostgreSQLOperateOption }
|
|
|
7
7
|
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
|
8
8
|
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
|
9
9
|
import { CreateEntityOption } from '../types/Translator';
|
|
10
|
-
import { DbStore } from '../types/dbStore';
|
|
10
|
+
import { DbStore, Plan } from '../types/dbStore';
|
|
11
11
|
export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
|
12
12
|
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
|
|
13
13
|
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
|
|
@@ -35,4 +35,16 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
|
|
|
35
35
|
connect(): Promise<void>;
|
|
36
36
|
disconnect(): Promise<void>;
|
|
37
37
|
initialize(option: CreateEntityOption): Promise<void>;
|
|
38
|
+
readSchema(): Promise<StorageSchema<ED>>;
|
|
39
|
+
/**
|
|
40
|
+
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
41
|
+
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
42
|
+
*/
|
|
43
|
+
makeUpgradePlan(): Promise<Plan>;
|
|
44
|
+
/**
|
|
45
|
+
* 比较两个schema的不同,这里计算的是new对old的增量
|
|
46
|
+
* @param schemaOld
|
|
47
|
+
* @param schemaNew
|
|
48
|
+
*/
|
|
49
|
+
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
|
38
50
|
}
|
package/lib/PostgreSQL/store.js
CHANGED
|
@@ -8,6 +8,12 @@ const translator_1 = require("./translator");
|
|
|
8
8
|
const lodash_1 = require("lodash");
|
|
9
9
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
10
10
|
const relation_1 = require("oak-domain/lib/store/relation");
|
|
11
|
+
const ToNumberAttrs = new Set([
|
|
12
|
+
'$$seq$$',
|
|
13
|
+
'$$createAt$$',
|
|
14
|
+
'$$updateAt$$',
|
|
15
|
+
'$$deleteAt$$',
|
|
16
|
+
]);
|
|
11
17
|
function convertGeoTextToObject(geoText) {
|
|
12
18
|
if (geoText.startsWith('POINT')) {
|
|
13
19
|
const coord = geoText.match(/(-?\d+\.?\d*)/g);
|
|
@@ -121,12 +127,19 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
121
127
|
const { type } = attributes[attr];
|
|
122
128
|
switch (type) {
|
|
123
129
|
case 'date':
|
|
124
|
-
case 'time':
|
|
130
|
+
case 'time':
|
|
131
|
+
case 'datetime': {
|
|
125
132
|
if (value instanceof Date) {
|
|
126
133
|
r[attr] = value.valueOf();
|
|
127
134
|
}
|
|
128
135
|
else {
|
|
129
|
-
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
r[attr] = parseInt(value, 10);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
(0, assert_1.default)(typeof value === 'number' || value === null);
|
|
141
|
+
r[attr] = value;
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
break;
|
|
132
145
|
}
|
|
@@ -203,6 +216,14 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
203
216
|
// PostgreSQL count 返回字符串
|
|
204
217
|
r[attr] = parseInt(value, 10);
|
|
205
218
|
}
|
|
219
|
+
else if (attr.startsWith("#sum") || attr.startsWith("#avg") || attr.startsWith("#min") || attr.startsWith("#max")) {
|
|
220
|
+
// PostgreSQL sum/avg/min/max 返回字符串
|
|
221
|
+
r[attr] = parseFloat(value);
|
|
222
|
+
}
|
|
223
|
+
else if (ToNumberAttrs.has(attr)) {
|
|
224
|
+
// PostgreSQL sum/avg/min/max 返回字符串
|
|
225
|
+
r[attr] = parseInt(value, 10);
|
|
226
|
+
}
|
|
206
227
|
else {
|
|
207
228
|
r[attr] = value;
|
|
208
229
|
}
|
|
@@ -315,15 +336,192 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
315
336
|
await this.connector.disconnect();
|
|
316
337
|
}
|
|
317
338
|
async initialize(option) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
// PG的DDL支持事务,所以这里直接用一个事务包裹所有的初始化操作
|
|
340
|
+
const txn = await this.connector.startTransaction({
|
|
341
|
+
isolationLevel: 'serializable',
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
const schema = this.getSchema();
|
|
345
|
+
let hasGeoType = false;
|
|
346
|
+
let hasChineseTsConfig = false;
|
|
347
|
+
for (const entity in schema) {
|
|
348
|
+
const { attributes, indexes } = schema[entity];
|
|
349
|
+
for (const attr in attributes) {
|
|
350
|
+
const { type } = attributes[attr];
|
|
351
|
+
if (type === 'geometry') {
|
|
352
|
+
hasGeoType = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const index of indexes || []) {
|
|
356
|
+
if (index.config?.tsConfig === 'chinese' || index.config?.tsConfig?.includes('chinese')) {
|
|
357
|
+
hasChineseTsConfig = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (hasGeoType) {
|
|
362
|
+
console.log('Initializing PostGIS extension for geometry support...');
|
|
363
|
+
await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
|
|
364
|
+
}
|
|
365
|
+
if (hasChineseTsConfig) {
|
|
366
|
+
console.log('Initializing Chinese text search configuration...');
|
|
367
|
+
const checkChineseConfigSql = `
|
|
368
|
+
SELECT COUNT(*) as cnt
|
|
369
|
+
FROM pg_catalog.pg_ts_config
|
|
370
|
+
WHERE cfgname = 'chinese';
|
|
371
|
+
`;
|
|
372
|
+
const result = await this.connector.exec(checkChineseConfigSql);
|
|
373
|
+
const count = parseInt(result[0][0]?.cnt || '0', 10);
|
|
374
|
+
if (count === 0) {
|
|
375
|
+
const createChineseConfigSql = `
|
|
376
|
+
CREATE EXTENSION IF NOT EXISTS zhparser;
|
|
377
|
+
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
|
|
378
|
+
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
|
|
379
|
+
`;
|
|
380
|
+
await this.connector.exec(createChineseConfigSql);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const entity in schema) {
|
|
384
|
+
const sqls = this.translator.translateCreateEntity(entity, option);
|
|
385
|
+
for (const sql of sqls) {
|
|
386
|
+
await this.connector.exec(sql, txn);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await this.connector.commitTransaction(txn);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
await this.connector.rollbackTransaction(txn);
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 从数据库中读取当前schema
|
|
397
|
+
readSchema() {
|
|
398
|
+
return this.translator.readSchema((sql) => this.connector.exec(sql));
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
402
|
+
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
403
|
+
*/
|
|
404
|
+
async makeUpgradePlan() {
|
|
405
|
+
const originSchema = await this.readSchema();
|
|
406
|
+
const plan = this.diffSchema(originSchema, this.translator.schema);
|
|
407
|
+
return plan;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 比较两个schema的不同,这里计算的是new对old的增量
|
|
411
|
+
* @param schemaOld
|
|
412
|
+
* @param schemaNew
|
|
413
|
+
*/
|
|
414
|
+
diffSchema(schemaOld, schemaNew) {
|
|
415
|
+
const plan = {
|
|
416
|
+
newTables: {},
|
|
417
|
+
newIndexes: {},
|
|
418
|
+
updatedIndexes: {},
|
|
419
|
+
updatedTables: {},
|
|
420
|
+
};
|
|
421
|
+
for (const table in schemaNew) {
|
|
422
|
+
// PostgreSQL 表名区分大小写(使用双引号时)
|
|
423
|
+
if (schemaOld[table]) {
|
|
424
|
+
const { attributes, indexes } = schemaOld[table];
|
|
425
|
+
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
|
|
426
|
+
const assignToUpdateTables = (attr, isNew) => {
|
|
427
|
+
const skipAttrs = ['$$seq$$', '$$createAt$$', '$$updateAt$$', '$$deleteAt$$', 'id'];
|
|
428
|
+
if (skipAttrs.includes(attr)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!plan.updatedTables[table]) {
|
|
432
|
+
plan.updatedTables[table] = {
|
|
433
|
+
attributes: {
|
|
434
|
+
[attr]: {
|
|
435
|
+
...attributesNew[attr],
|
|
436
|
+
isNew,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
plan.updatedTables[table].attributes[attr] = {
|
|
443
|
+
...attributesNew[attr],
|
|
444
|
+
isNew,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
for (const attr in attributesNew) {
|
|
449
|
+
if (attributes[attr]) {
|
|
450
|
+
// 比较两次创建的属性定义是否一致
|
|
451
|
+
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
|
452
|
+
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
|
453
|
+
if (!this.translator.compareSql(sql1, sql2)) {
|
|
454
|
+
assignToUpdateTables(attr, false);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
assignToUpdateTables(attr, true);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (indexesNew) {
|
|
462
|
+
const assignToIndexes = (index, isNew) => {
|
|
463
|
+
if (isNew) {
|
|
464
|
+
if (plan.newIndexes[table]) {
|
|
465
|
+
plan.newIndexes[table].push(index);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
plan.newIndexes[table] = [index];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
if (plan.updatedIndexes[table]) {
|
|
473
|
+
plan.updatedIndexes[table].push(index);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
plan.updatedIndexes[table] = [index];
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const compareConfig = (config1, config2) => {
|
|
481
|
+
const unique1 = config1?.unique || false;
|
|
482
|
+
const unique2 = config2?.unique || false;
|
|
483
|
+
if (unique1 !== unique2) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const type1 = config1?.type || 'btree';
|
|
487
|
+
const type2 = config2?.type || 'btree';
|
|
488
|
+
// tsConfig 比较
|
|
489
|
+
const tsConfig1 = config1?.tsConfig;
|
|
490
|
+
const tsConfig2 = config2?.tsConfig;
|
|
491
|
+
if (JSON.stringify(tsConfig1) !== JSON.stringify(tsConfig2)) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return type1 === type2;
|
|
495
|
+
};
|
|
496
|
+
for (const index of indexesNew) {
|
|
497
|
+
const { name, config, attributes: indexAttrs } = index;
|
|
498
|
+
const origin = indexes?.find(ele => ele.name === name);
|
|
499
|
+
if (origin) {
|
|
500
|
+
if (JSON.stringify(indexAttrs) !== JSON.stringify(origin.attributes)) {
|
|
501
|
+
assignToIndexes(index, false);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
if (!compareConfig(config, origin.config)) {
|
|
505
|
+
assignToIndexes(index, false);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
assignToIndexes(index, true);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
plan.newTables[table] = {
|
|
517
|
+
attributes: schemaNew[table].attributes,
|
|
518
|
+
};
|
|
519
|
+
if (schemaNew[table].indexes) {
|
|
520
|
+
plan.newIndexes[table] = schemaNew[table].indexes;
|
|
521
|
+
}
|
|
325
522
|
}
|
|
326
523
|
}
|
|
524
|
+
return plan;
|
|
327
525
|
}
|
|
328
526
|
}
|
|
329
527
|
exports.PostgreSQLStore = PostgreSQLStore;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema } from "oak-domain/lib/types";
|
|
1
|
+
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Attribute } from "oak-domain/lib/types";
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
3
|
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
|
|
4
4
|
import { SqlOperateOption, SqlSelectOption, SqlTranslator } from "../sqlTranslator";
|
|
@@ -80,4 +80,24 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
|
|
|
80
80
|
translateUpsert<T extends keyof ED>(entity: T, data: ED[T]['CreateMulti']['data'], conflictKeys: string[], updateAttrs?: string[]): string;
|
|
81
81
|
protected populateUpdateStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
|
|
82
82
|
protected populateRemoveStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
|
|
83
|
+
/**
|
|
84
|
+
* 将 PostgreSQL 返回的 Type 回译成 oak 的类型,是 populateDataTypeDef 的反函数
|
|
85
|
+
* @param type PostgreSQL 类型字符串
|
|
86
|
+
*/
|
|
87
|
+
private reTranslateToAttribute;
|
|
88
|
+
/**
|
|
89
|
+
* 从 PostgreSQL 数据库读取当前的 schema 结构
|
|
90
|
+
*/
|
|
91
|
+
readSchema(execFn: (sql: string) => Promise<any>): Promise<StorageSchema<ED>>;
|
|
92
|
+
/**
|
|
93
|
+
* 将属性定义转换为 PostgreSQL DDL 语句
|
|
94
|
+
* @param attr 属性名
|
|
95
|
+
* @param attrDef 属性定义
|
|
96
|
+
*/
|
|
97
|
+
translateAttributeDef(attr: string, attrDef: Attribute): string;
|
|
98
|
+
/**
|
|
99
|
+
* 比较两个 SQL 语句是否等价(用于 schema diff)
|
|
100
|
+
* 忽略空格、大小写等格式差异
|
|
101
|
+
*/
|
|
102
|
+
compareSql(sql1: string, sql2: string): boolean;
|
|
83
103
|
}
|
|
@@ -741,6 +741,7 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
741
741
|
else if (typeof value === 'number') {
|
|
742
742
|
return `${value}`;
|
|
743
743
|
}
|
|
744
|
+
(0, assert_1.default)(typeof value === 'string', 'Invalid date/time value');
|
|
744
745
|
return `'${(new Date(value)).valueOf()}'`;
|
|
745
746
|
}
|
|
746
747
|
case 'object':
|
|
@@ -967,12 +968,8 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
967
968
|
}
|
|
968
969
|
indexSql += '(';
|
|
969
970
|
const indexColumns = [];
|
|
970
|
-
let includeDeleteAt = false;
|
|
971
971
|
for (const indexAttr of indexAttrs) {
|
|
972
972
|
const { name: attrName, direction } = indexAttr;
|
|
973
|
-
if (attrName === '$$deleteAt$$') {
|
|
974
|
-
includeDeleteAt = true;
|
|
975
|
-
}
|
|
976
973
|
if (indexType === 'fulltext') {
|
|
977
974
|
// 全文索引:使用 to_tsvector
|
|
978
975
|
indexColumns.push(`to_tsvector('${tsLang}', COALESCE("${attrName}", ''))`);
|
|
@@ -986,10 +983,6 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
986
983
|
indexColumns.push(col);
|
|
987
984
|
}
|
|
988
985
|
}
|
|
989
|
-
// 非特殊索引自动包含 deleteAt
|
|
990
|
-
if (!includeDeleteAt && !indexType) {
|
|
991
|
-
indexColumns.push('"$$deleteAt$$"');
|
|
992
|
-
}
|
|
993
986
|
indexSql += indexColumns.join(', ');
|
|
994
987
|
indexSql += ');';
|
|
995
988
|
sqls.push(indexSql);
|
|
@@ -1766,5 +1759,401 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1766
1759
|
// 这个方法不应该被直接调用了,因为translateRemove已经重写
|
|
1767
1760
|
throw new Error('populateRemoveStmt should not be called directly in PostgreSQL. Use translateRemove instead.');
|
|
1768
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* 将 PostgreSQL 返回的 Type 回译成 oak 的类型,是 populateDataTypeDef 的反函数
|
|
1764
|
+
* @param type PostgreSQL 类型字符串
|
|
1765
|
+
*/
|
|
1766
|
+
reTranslateToAttribute(type) {
|
|
1767
|
+
// 处理带长度的类型:character varying(255), character(10)
|
|
1768
|
+
const varcharMatch = /^character varying\((\d+)\)$/.exec(type);
|
|
1769
|
+
if (varcharMatch) {
|
|
1770
|
+
return {
|
|
1771
|
+
type: 'varchar',
|
|
1772
|
+
params: {
|
|
1773
|
+
length: parseInt(varcharMatch[1], 10),
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
const charMatch = /^character\((\d+)\)$/.exec(type);
|
|
1778
|
+
if (charMatch) {
|
|
1779
|
+
return {
|
|
1780
|
+
type: 'char',
|
|
1781
|
+
params: {
|
|
1782
|
+
length: parseInt(charMatch[1], 10),
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
// 处理带精度和小数位的类型:numeric(10,2)
|
|
1787
|
+
const numericWithScaleMatch = /^numeric\((\d+),(\d+)\)$/.exec(type);
|
|
1788
|
+
if (numericWithScaleMatch) {
|
|
1789
|
+
return {
|
|
1790
|
+
type: 'decimal',
|
|
1791
|
+
params: {
|
|
1792
|
+
precision: parseInt(numericWithScaleMatch[1], 10),
|
|
1793
|
+
scale: parseInt(numericWithScaleMatch[2], 10),
|
|
1794
|
+
},
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
// 处理只带精度的类型:numeric(10), timestamp(6)
|
|
1798
|
+
const numericMatch = /^numeric\((\d+)\)$/.exec(type);
|
|
1799
|
+
if (numericMatch) {
|
|
1800
|
+
return {
|
|
1801
|
+
type: 'decimal',
|
|
1802
|
+
params: {
|
|
1803
|
+
precision: parseInt(numericMatch[1], 10),
|
|
1804
|
+
scale: 0,
|
|
1805
|
+
},
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const timestampMatch = /^timestamp\((\d+)\) without time zone$/.exec(type);
|
|
1809
|
+
if (timestampMatch) {
|
|
1810
|
+
return {
|
|
1811
|
+
type: 'timestamp',
|
|
1812
|
+
params: {
|
|
1813
|
+
precision: parseInt(timestampMatch[1], 10),
|
|
1814
|
+
},
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
const timeMatch = /^time\((\d+)\) without time zone$/.exec(type);
|
|
1818
|
+
if (timeMatch) {
|
|
1819
|
+
return {
|
|
1820
|
+
type: 'time',
|
|
1821
|
+
params: {
|
|
1822
|
+
precision: parseInt(timeMatch[1], 10),
|
|
1823
|
+
},
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
// PostgreSQL 类型映射到 oak 类型
|
|
1827
|
+
const typeMap = {
|
|
1828
|
+
'bigint': 'bigint',
|
|
1829
|
+
'integer': 'integer',
|
|
1830
|
+
'smallint': 'smallint',
|
|
1831
|
+
'real': 'real',
|
|
1832
|
+
'double precision': 'double precision',
|
|
1833
|
+
'boolean': 'boolean',
|
|
1834
|
+
'text': 'text',
|
|
1835
|
+
'jsonb': 'object',
|
|
1836
|
+
'json': 'object',
|
|
1837
|
+
'bytea': 'bytea',
|
|
1838
|
+
'character varying': 'varchar',
|
|
1839
|
+
'character': 'char',
|
|
1840
|
+
'timestamp without time zone': 'timestamp',
|
|
1841
|
+
'time without time zone': 'time',
|
|
1842
|
+
'date': 'date',
|
|
1843
|
+
'uuid': 'uuid',
|
|
1844
|
+
'geometry': 'geometry',
|
|
1845
|
+
'numeric': 'decimal',
|
|
1846
|
+
};
|
|
1847
|
+
const mappedType = typeMap[type];
|
|
1848
|
+
if (mappedType) {
|
|
1849
|
+
return { type: mappedType };
|
|
1850
|
+
}
|
|
1851
|
+
// 如果是用户定义的枚举类型,返回 enum(具体值需要额外查询)
|
|
1852
|
+
// 这里先返回基础类型,枚举值在 readSchema 中单独处理
|
|
1853
|
+
return { type: type };
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* 从 PostgreSQL 数据库读取当前的 schema 结构
|
|
1857
|
+
*/
|
|
1858
|
+
async readSchema(execFn) {
|
|
1859
|
+
const result = {};
|
|
1860
|
+
// 1. 获取所有表
|
|
1861
|
+
const tablesSql = `
|
|
1862
|
+
SELECT tablename
|
|
1863
|
+
FROM pg_tables
|
|
1864
|
+
WHERE schemaname = 'public'
|
|
1865
|
+
ORDER BY tablename;
|
|
1866
|
+
`;
|
|
1867
|
+
const [tablesResult] = await execFn(tablesSql);
|
|
1868
|
+
for (const tableRow of tablesResult) {
|
|
1869
|
+
const tableName = tableRow.tablename;
|
|
1870
|
+
// 2. 获取表的列信息
|
|
1871
|
+
const columnsSql = `
|
|
1872
|
+
SELECT
|
|
1873
|
+
column_name,
|
|
1874
|
+
data_type,
|
|
1875
|
+
character_maximum_length,
|
|
1876
|
+
numeric_precision,
|
|
1877
|
+
numeric_scale,
|
|
1878
|
+
is_nullable,
|
|
1879
|
+
column_default,
|
|
1880
|
+
udt_name
|
|
1881
|
+
FROM information_schema.columns
|
|
1882
|
+
WHERE table_schema = 'public'
|
|
1883
|
+
AND table_name = '${tableName}'
|
|
1884
|
+
ORDER BY ordinal_position;
|
|
1885
|
+
`;
|
|
1886
|
+
const [columnsResult] = await execFn(columnsSql);
|
|
1887
|
+
const attributes = {};
|
|
1888
|
+
for (const col of columnsResult) {
|
|
1889
|
+
const { column_name: colName, data_type: dataType, character_maximum_length: maxLength, numeric_precision: precision, numeric_scale: scale, is_nullable: isNullable, column_default: defaultValue, udt_name: udtName, } = col;
|
|
1890
|
+
let attr;
|
|
1891
|
+
// 处理用户定义类型(枚举)
|
|
1892
|
+
if (dataType === 'USER-DEFINED') {
|
|
1893
|
+
const enumSql = `
|
|
1894
|
+
SELECT e.enumlabel
|
|
1895
|
+
FROM pg_type t
|
|
1896
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
1897
|
+
WHERE t.typname = '${udtName}'
|
|
1898
|
+
ORDER BY e.enumsortorder;
|
|
1899
|
+
`;
|
|
1900
|
+
const [enumResult] = await execFn(enumSql);
|
|
1901
|
+
const enumeration = enumResult.map((r) => r.enumlabel);
|
|
1902
|
+
attr = {
|
|
1903
|
+
type: 'enum',
|
|
1904
|
+
enumeration,
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
// 构建完整的类型字符串
|
|
1909
|
+
let fullType = dataType;
|
|
1910
|
+
const integerTypes = ['bigint', 'integer', 'smallint', 'serial', 'bigserial', 'smallserial'];
|
|
1911
|
+
if (maxLength && !integerTypes.includes(dataType)) {
|
|
1912
|
+
fullType = `${dataType}(${maxLength})`;
|
|
1913
|
+
}
|
|
1914
|
+
else if (precision !== null && scale !== null && !integerTypes.includes(dataType)) {
|
|
1915
|
+
fullType = `${dataType}(${precision},${scale})`;
|
|
1916
|
+
}
|
|
1917
|
+
else if (precision !== null && !integerTypes.includes(dataType)) {
|
|
1918
|
+
fullType = `${dataType}(${precision})`;
|
|
1919
|
+
}
|
|
1920
|
+
attr = this.reTranslateToAttribute(fullType);
|
|
1921
|
+
}
|
|
1922
|
+
// ========== 类型还原逻辑 ==========
|
|
1923
|
+
// 框架将某些语义类型存储为 bigint,需要根据列名还原
|
|
1924
|
+
if (attr.type === 'bigint') {
|
|
1925
|
+
// 1. 检查是否是序列列
|
|
1926
|
+
if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) {
|
|
1927
|
+
attr.type = 'sequence';
|
|
1928
|
+
attr.sequenceStart = 10000; // 默认起始值
|
|
1929
|
+
}
|
|
1930
|
+
// 2. 检查是否是时间戳列
|
|
1931
|
+
else if (['$$createAt$$', '$$updateAt$$', '$$deleteAt$$'].includes(colName)) {
|
|
1932
|
+
attr.type = 'datetime';
|
|
1933
|
+
}
|
|
1934
|
+
// 3. 检查其他可能的时间类型列(根据命名约定)
|
|
1935
|
+
else if (colName.endsWith('At') || colName.endsWith('Time')) {
|
|
1936
|
+
// 可选:根据业务约定判断是否应该是 datetime
|
|
1937
|
+
// 这里保守处理,只转换框架标准字段
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
// 处理约束 - 只在为 true 时添加
|
|
1941
|
+
if (isNullable === 'NO') {
|
|
1942
|
+
attr.notNull = true;
|
|
1943
|
+
}
|
|
1944
|
+
// 处理默认值
|
|
1945
|
+
if (defaultValue && !defaultValue.includes('nextval')) {
|
|
1946
|
+
let cleanDefault = defaultValue.replace(/::[a-z]+/gi, '').replace(/'/g, '');
|
|
1947
|
+
if (cleanDefault === 'true') {
|
|
1948
|
+
attr.default = true;
|
|
1949
|
+
}
|
|
1950
|
+
else if (cleanDefault === 'false') {
|
|
1951
|
+
attr.default = false;
|
|
1952
|
+
}
|
|
1953
|
+
else if (!isNaN(Number(cleanDefault))) {
|
|
1954
|
+
attr.default = Number(cleanDefault);
|
|
1955
|
+
}
|
|
1956
|
+
else if (cleanDefault !== '') {
|
|
1957
|
+
attr.default = cleanDefault;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
// 检查唯一约束
|
|
1961
|
+
const uniqueSql = `
|
|
1962
|
+
SELECT COUNT(*) as cnt
|
|
1963
|
+
FROM pg_index ix
|
|
1964
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
1965
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
1966
|
+
WHERE t.relname = '${tableName}'
|
|
1967
|
+
AND a.attname = '${colName}'
|
|
1968
|
+
AND ix.indisunique = true
|
|
1969
|
+
AND NOT ix.indisprimary
|
|
1970
|
+
AND array_length(ix.indkey, 1) = 1;
|
|
1971
|
+
`;
|
|
1972
|
+
const [uniqueResult] = await execFn(uniqueSql);
|
|
1973
|
+
const uniqueCount = parseInt(uniqueResult[0]?.cnt || '0', 10);
|
|
1974
|
+
if (uniqueCount > 0) {
|
|
1975
|
+
attr.unique = true;
|
|
1976
|
+
}
|
|
1977
|
+
attributes[colName] = attr;
|
|
1978
|
+
}
|
|
1979
|
+
// 3. 获取索引信息
|
|
1980
|
+
const indexesSql = `
|
|
1981
|
+
SELECT
|
|
1982
|
+
i.relname as index_name,
|
|
1983
|
+
ix.indisunique as is_unique,
|
|
1984
|
+
am.amname as index_type,
|
|
1985
|
+
pg_get_indexdef(ix.indexrelid) as index_def
|
|
1986
|
+
FROM pg_class t
|
|
1987
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
1988
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
1989
|
+
JOIN pg_am am ON i.relam = am.oid
|
|
1990
|
+
WHERE t.relname = '${tableName}'
|
|
1991
|
+
AND t.relkind = 'r'
|
|
1992
|
+
AND i.relname NOT LIKE '%_pkey'
|
|
1993
|
+
AND NOT ix.indisprimary
|
|
1994
|
+
ORDER BY i.relname;
|
|
1995
|
+
`;
|
|
1996
|
+
const [indexesResult] = await execFn(indexesSql);
|
|
1997
|
+
if (indexesResult.length > 0) {
|
|
1998
|
+
const indexes = [];
|
|
1999
|
+
for (const row of indexesResult) {
|
|
2000
|
+
const { index_name: indexName, is_unique: isUnique, index_type: indexType, index_def: indexDef } = row;
|
|
2001
|
+
// 解析索引定义以获取列名和配置
|
|
2002
|
+
const index = {
|
|
2003
|
+
name: indexName,
|
|
2004
|
+
attributes: [],
|
|
2005
|
+
};
|
|
2006
|
+
// 解析索引定义字符串
|
|
2007
|
+
// 示例: CREATE INDEX "user_index_fulltext_chinese" ON public."user" USING gin (to_tsvector('chinese'::regconfig, (COALESCE(name, ''::text) || ' '::text) || COALESCE(nickname, ''::text)))
|
|
2008
|
+
if (indexType === 'gin' && indexDef.includes('to_tsvector')) {
|
|
2009
|
+
// 全文索引
|
|
2010
|
+
index.config = { type: 'fulltext' };
|
|
2011
|
+
// 提取 tsConfig
|
|
2012
|
+
const tsConfigMatch = indexDef.match(/to_tsvector\('([^']+)'/);
|
|
2013
|
+
if (tsConfigMatch) {
|
|
2014
|
+
const tsConfig = tsConfigMatch[1];
|
|
2015
|
+
index.config.tsConfig = tsConfig;
|
|
2016
|
+
}
|
|
2017
|
+
// 提取列名(从 COALESCE 中)
|
|
2018
|
+
const columnMatches = indexDef.matchAll(/COALESCE\("?([^",\s]+)"?/g);
|
|
2019
|
+
const columns = Array.from(columnMatches, m => m[1]);
|
|
2020
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
2021
|
+
// 处理多语言索引的情况:移除语言后缀
|
|
2022
|
+
// 例如: user_index_fulltext_chinese -> index_fulltext
|
|
2023
|
+
const nameParts = indexName.split('_');
|
|
2024
|
+
if (nameParts.length > 2) {
|
|
2025
|
+
const possibleLang = nameParts[nameParts.length - 1];
|
|
2026
|
+
// 如果最后一部分是语言代码,移除它
|
|
2027
|
+
if (['chinese', 'english', 'simple', 'german', 'french', 'spanish', 'russian', 'japanese'].includes(possibleLang)) {
|
|
2028
|
+
index.name = nameParts.slice(0, -1).join('_');
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
else if (indexType === 'gist') {
|
|
2033
|
+
// 空间索引
|
|
2034
|
+
index.config = { type: 'spatial' };
|
|
2035
|
+
// 提取列名
|
|
2036
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
2037
|
+
if (columnMatch) {
|
|
2038
|
+
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
|
2039
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
else if (indexType === 'hash') {
|
|
2043
|
+
// 哈希索引
|
|
2044
|
+
index.config = { type: 'hash' };
|
|
2045
|
+
// 提取列名
|
|
2046
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
2047
|
+
if (columnMatch) {
|
|
2048
|
+
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
|
2049
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
// B-tree 索引(默认)
|
|
2054
|
+
// 提取列名和排序方向
|
|
2055
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
2056
|
+
if (columnMatch) {
|
|
2057
|
+
const columnDefs = columnMatch[1].split(',');
|
|
2058
|
+
index.attributes = columnDefs.map(colDef => {
|
|
2059
|
+
const trimmed = colDef.trim().replace(/"/g, '');
|
|
2060
|
+
const parts = trimmed.split(/\s+/);
|
|
2061
|
+
const attr = { name: parts[0] };
|
|
2062
|
+
// 检查排序方向
|
|
2063
|
+
if (parts.includes('DESC')) {
|
|
2064
|
+
attr.direction = 'DESC';
|
|
2065
|
+
}
|
|
2066
|
+
else if (parts.includes('ASC')) {
|
|
2067
|
+
attr.direction = 'ASC';
|
|
2068
|
+
}
|
|
2069
|
+
return attr;
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
// 如果是唯一索引
|
|
2073
|
+
if (isUnique) {
|
|
2074
|
+
index.config = { unique: true };
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// 移除表名前缀(如果存在)
|
|
2078
|
+
// 例如: user_index_fulltext -> index_fulltext
|
|
2079
|
+
if (index.name.startsWith(`${tableName}_`)) {
|
|
2080
|
+
index.name = index.name.substring(tableName.length + 1);
|
|
2081
|
+
}
|
|
2082
|
+
indexes.push(index);
|
|
2083
|
+
}
|
|
2084
|
+
Object.assign(result, {
|
|
2085
|
+
[tableName]: {
|
|
2086
|
+
attributes,
|
|
2087
|
+
indexes,
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
Object.assign(result, {
|
|
2093
|
+
[tableName]: {
|
|
2094
|
+
attributes,
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return result;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* 将属性定义转换为 PostgreSQL DDL 语句
|
|
2103
|
+
* @param attr 属性名
|
|
2104
|
+
* @param attrDef 属性定义
|
|
2105
|
+
*/
|
|
2106
|
+
translateAttributeDef(attr, attrDef) {
|
|
2107
|
+
let sql = `"${attr}" `;
|
|
2108
|
+
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
|
2109
|
+
// 处理序列类型(IDENTITY)
|
|
2110
|
+
if (type === 'sequence' || (typeof sequenceStart === 'number')) {
|
|
2111
|
+
sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart || 10000}) UNIQUE`;
|
|
2112
|
+
return sql;
|
|
2113
|
+
}
|
|
2114
|
+
// 处理枚举类型
|
|
2115
|
+
if (type === 'enum') {
|
|
2116
|
+
(0, assert_1.default)(enumeration, 'Enum type requires enumeration values');
|
|
2117
|
+
sql += `enum(${enumeration.map(v => `'${v}'`).join(',')})`;
|
|
2118
|
+
}
|
|
2119
|
+
else {
|
|
2120
|
+
sql += this.populateDataTypeDef(type, params, enumeration);
|
|
2121
|
+
}
|
|
2122
|
+
// NOT NULL 约束
|
|
2123
|
+
if (notNull || type === 'geometry') {
|
|
2124
|
+
sql += ' NOT NULL';
|
|
2125
|
+
}
|
|
2126
|
+
// UNIQUE 约束
|
|
2127
|
+
if (unique) {
|
|
2128
|
+
sql += ' UNIQUE';
|
|
2129
|
+
}
|
|
2130
|
+
// 默认值
|
|
2131
|
+
if (defaultValue !== undefined && !sequenceStart) {
|
|
2132
|
+
(0, assert_1.default)(type !== 'ref', 'ref type should not have default value');
|
|
2133
|
+
sql += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
|
|
2134
|
+
}
|
|
2135
|
+
// 主键
|
|
2136
|
+
if (attr === 'id') {
|
|
2137
|
+
sql += ' PRIMARY KEY';
|
|
2138
|
+
}
|
|
2139
|
+
return sql;
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* 比较两个 SQL 语句是否等价(用于 schema diff)
|
|
2143
|
+
* 忽略空格、大小写等格式差异
|
|
2144
|
+
*/
|
|
2145
|
+
compareSql(sql1, sql2) {
|
|
2146
|
+
// 标准化 SQL:移除多余空格,统一大小写
|
|
2147
|
+
const normalize = (sql) => {
|
|
2148
|
+
return sql
|
|
2149
|
+
.replace(/\s+/g, ' ') // 多个空格合并为一个
|
|
2150
|
+
.replace(/\(\s+/g, '(') // 移除括号后的空格
|
|
2151
|
+
.replace(/\s+\)/g, ')') // 移除括号前的空格
|
|
2152
|
+
.replace(/,\s+/g, ',') // 移除逗号后的空格
|
|
2153
|
+
.trim()
|
|
2154
|
+
.toLowerCase();
|
|
2155
|
+
};
|
|
2156
|
+
return normalize(sql1) === normalize(sql2);
|
|
2157
|
+
}
|
|
1769
2158
|
}
|
|
1770
2159
|
exports.PostgreSQLTranslator = PostgreSQLTranslator;
|
package/lib/sqlTranslator.js
CHANGED
|
@@ -72,6 +72,8 @@ class SqlTranslator {
|
|
|
72
72
|
name: `${entity}_trigger_uuid_auto_create`,
|
|
73
73
|
attributes: [{
|
|
74
74
|
name: types_1.TriggerUuidAttribute,
|
|
75
|
+
}, {
|
|
76
|
+
name: types_1.DeleteAtAttribute,
|
|
75
77
|
}]
|
|
76
78
|
},
|
|
77
79
|
];
|
|
@@ -84,7 +86,7 @@ class SqlTranslator {
|
|
|
84
86
|
attributes: [{
|
|
85
87
|
name: attr,
|
|
86
88
|
}, {
|
|
87
|
-
name:
|
|
89
|
+
name: types_1.DeleteAtAttribute,
|
|
88
90
|
}]
|
|
89
91
|
});
|
|
90
92
|
}
|
|
@@ -100,7 +102,7 @@ class SqlTranslator {
|
|
|
100
102
|
}, {
|
|
101
103
|
name: 'entityId',
|
|
102
104
|
}, {
|
|
103
|
-
name:
|
|
105
|
+
name: types_1.DeleteAtAttribute,
|
|
104
106
|
}]
|
|
105
107
|
});
|
|
106
108
|
}
|
|
@@ -113,7 +115,7 @@ class SqlTranslator {
|
|
|
113
115
|
attributes: [{
|
|
114
116
|
name: attr,
|
|
115
117
|
}, {
|
|
116
|
-
name:
|
|
118
|
+
name: types_1.DeleteAtAttribute,
|
|
117
119
|
}]
|
|
118
120
|
});
|
|
119
121
|
}
|
|
@@ -129,7 +131,7 @@ class SqlTranslator {
|
|
|
129
131
|
}, {
|
|
130
132
|
name: 'expiresAt',
|
|
131
133
|
}, {
|
|
132
|
-
name:
|
|
134
|
+
name: types_1.DeleteAtAttribute,
|
|
133
135
|
}]
|
|
134
136
|
});
|
|
135
137
|
}
|
package/lib/types/dbStore.d.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
import { EntityDict } from
|
|
1
|
+
import { Attribute, EntityDict, Index, OperateOption, OperationResult, StorageSchema, TxnOption } from 'oak-domain/lib/types';
|
|
2
|
+
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
2
3
|
import { AsyncContext, AsyncRowStore } from "oak-domain/lib/store/AsyncRowStore";
|
|
3
4
|
import { CreateEntityOption } from "./Translator";
|
|
4
|
-
|
|
5
|
+
import { AggregationResult, SelectOption } from "oak-domain/lib/types";
|
|
6
|
+
export type Plan = {
|
|
7
|
+
newTables: Record<string, {
|
|
8
|
+
attributes: Record<string, Attribute>;
|
|
9
|
+
}>;
|
|
10
|
+
newIndexes: Record<string, Index<any>[]>;
|
|
11
|
+
updatedTables: Record<string, {
|
|
12
|
+
attributes: Record<string, Attribute & {
|
|
13
|
+
isNew: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
}>;
|
|
16
|
+
updatedIndexes: Record<string, Index<any>[]>;
|
|
17
|
+
};
|
|
18
|
+
export interface DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
|
|
5
19
|
connect: () => Promise<void>;
|
|
6
20
|
disconnect: () => Promise<void>;
|
|
7
21
|
initialize(options: CreateEntityOption): Promise<void>;
|
|
22
|
+
aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
|
|
23
|
+
operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
|
|
24
|
+
select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
|
|
25
|
+
count<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: SelectOption): Promise<number>;
|
|
26
|
+
begin(option?: TxnOption): Promise<string>;
|
|
27
|
+
commit(txnId: string): Promise<void>;
|
|
28
|
+
rollback(txnId: string): Promise<void>;
|
|
29
|
+
readSchema(): Promise<StorageSchema<ED>>;
|
|
30
|
+
makeUpgradePlan(): Promise<Plan>;
|
|
31
|
+
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
|
8
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-db",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.13",
|
|
4
4
|
"description": "oak-db",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"author": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"lodash": "^4.17.21",
|
|
19
19
|
"mysql": "^2.18.1",
|
|
20
20
|
"mysql2": "^2.3.3",
|
|
21
|
-
"oak-domain": "^5.1.
|
|
21
|
+
"oak-domain": "^5.1.34",
|
|
22
22
|
"pg": "^8.16.3",
|
|
23
23
|
"uuid": "^8.3.2"
|
|
24
24
|
},
|