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.
@@ -1,4 +1,4 @@
1
- import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult, Attribute, Index } from 'oak-domain/lib/types';
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 {};
@@ -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)) {
@@ -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 attrText = `\`${refDict[refId][0]}\`.\`${refAttr}\``;
876
- result = this.translateAttrInExpression(entity, (expr)['#refAttr'], attrText);
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
  }
@@ -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
- r[attr] = value;
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
- const schema = this.getSchema();
319
- // 可选:先创建 PostGIS 扩展
320
- // await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
321
- for (const entity in schema) {
322
- const sqls = this.translator.translateCreateEntity(entity, option);
323
- for (const sql of sqls) {
324
- await this.connector.exec(sql);
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;
@@ -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: '$$deleteAt$$',
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: '$$deleteAt$$',
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: '$$deleteAt$$',
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: '$$deleteAt$$',
134
+ name: types_1.DeleteAtAttribute,
133
135
  }]
134
136
  });
135
137
  }
@@ -1,8 +1,32 @@
1
- import { EntityDict } from "oak-domain/lib/base-app-domain";
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
- export interface DbStore<ED extends EntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
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.12",
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.33",
21
+ "oak-domain": "^5.1.34",
22
22
  "pg": "^8.16.3",
23
23
  "uuid": "^8.3.2"
24
24
  },