oak-db 3.3.12 → 3.3.14
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/connector.js +9 -14
- package/lib/MySQL/store.d.ts +5 -15
- package/lib/MySQL/store.js +11 -2
- package/lib/MySQL/translator.js +22 -11
- package/lib/PostgreSQL/connector.d.ts +0 -4
- package/lib/PostgreSQL/connector.js +29 -58
- package/lib/PostgreSQL/store.d.ts +16 -1
- package/lib/PostgreSQL/store.js +247 -7
- package/lib/PostgreSQL/translator.d.ts +39 -7
- package/lib/PostgreSQL/translator.js +480 -225
- package/lib/sqlTranslator.d.ts +5 -1
- package/lib/sqlTranslator.js +18 -8
- package/lib/types/dbStore.d.ts +27 -2
- package/package.json +2 -2
|
@@ -7,7 +7,6 @@ const util_1 = require("util");
|
|
|
7
7
|
const lodash_1 = require("lodash");
|
|
8
8
|
const types_1 = require("oak-domain/lib/types");
|
|
9
9
|
const sqlTranslator_1 = require("../sqlTranslator");
|
|
10
|
-
const relation_1 = require("oak-domain/lib/store/relation");
|
|
11
10
|
const GeoTypes = [
|
|
12
11
|
{
|
|
13
12
|
type: 'point',
|
|
@@ -741,6 +740,7 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
741
740
|
else if (typeof value === 'number') {
|
|
742
741
|
return `${value}`;
|
|
743
742
|
}
|
|
743
|
+
(0, assert_1.default)(typeof value === 'string', 'Invalid date/time value');
|
|
744
744
|
return `'${(new Date(value)).valueOf()}'`;
|
|
745
745
|
}
|
|
746
746
|
case 'object':
|
|
@@ -967,12 +967,8 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
967
967
|
}
|
|
968
968
|
indexSql += '(';
|
|
969
969
|
const indexColumns = [];
|
|
970
|
-
let includeDeleteAt = false;
|
|
971
970
|
for (const indexAttr of indexAttrs) {
|
|
972
971
|
const { name: attrName, direction } = indexAttr;
|
|
973
|
-
if (attrName === '$$deleteAt$$') {
|
|
974
|
-
includeDeleteAt = true;
|
|
975
|
-
}
|
|
976
972
|
if (indexType === 'fulltext') {
|
|
977
973
|
// 全文索引:使用 to_tsvector
|
|
978
974
|
indexColumns.push(`to_tsvector('${tsLang}', COALESCE("${attrName}", ''))`);
|
|
@@ -986,10 +982,6 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
986
982
|
indexColumns.push(col);
|
|
987
983
|
}
|
|
988
984
|
}
|
|
989
|
-
// 非特殊索引自动包含 deleteAt
|
|
990
|
-
if (!includeDeleteAt && !indexType) {
|
|
991
|
-
indexColumns.push('"$$deleteAt$$"');
|
|
992
|
-
}
|
|
993
985
|
indexSql += indexColumns.join(', ');
|
|
994
986
|
indexSql += ');';
|
|
995
987
|
sqls.push(indexSql);
|
|
@@ -1490,10 +1482,11 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1490
1482
|
translateUpdate(entity, operation, option) {
|
|
1491
1483
|
const { attributes } = this.schema[entity];
|
|
1492
1484
|
const { filter, sorter, indexFrom, count, data } = operation;
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1485
|
+
// 参数验证
|
|
1486
|
+
this.validateOperationParams(sorter, indexFrom, count);
|
|
1487
|
+
const mainTable = this.getStorageName(entity);
|
|
1488
|
+
const mainAlias = `${entity}_1`;
|
|
1489
|
+
// 构建 SET 子句
|
|
1497
1490
|
const setClauses = [];
|
|
1498
1491
|
for (const attr in data) {
|
|
1499
1492
|
(0, assert_1.default)(attributes.hasOwnProperty(attr));
|
|
@@ -1501,73 +1494,93 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1501
1494
|
setClauses.push(`"${attr}" = ${value}`);
|
|
1502
1495
|
}
|
|
1503
1496
|
const setClause = setClauses.join(', ');
|
|
1504
|
-
//
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1497
|
+
// 统一使用子查询方案
|
|
1498
|
+
const subqueryOperation = {
|
|
1499
|
+
data: { [types_1.PrimaryKeyAttribute]: 1 },
|
|
1500
|
+
filter: filter,
|
|
1501
|
+
indexFrom: indexFrom,
|
|
1502
|
+
count: count
|
|
1503
|
+
};
|
|
1504
|
+
const subquery = this.translateSelect(entity, subqueryOperation, { includedDeleted: option?.includedDeleted });
|
|
1505
|
+
let sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause} WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
|
|
1506
|
+
// 添加 RETURNING 子句
|
|
1507
|
+
return this.appendReturningClause(sql, entity, mainAlias, option);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* 将 projection 转换为 RETURNING 子句的列列表
|
|
1511
|
+
* @param entity 实体名
|
|
1512
|
+
* @param alias 表别名
|
|
1513
|
+
* @param projection 投影定义
|
|
1514
|
+
*/
|
|
1515
|
+
buildReturningClause(entity, alias, projection) {
|
|
1516
|
+
const columns = [];
|
|
1517
|
+
const { attributes } = this.schema[entity];
|
|
1518
|
+
for (const attr in projection) {
|
|
1519
|
+
// if (!attributes[attr]) {
|
|
1520
|
+
// // 如果存在entity这个attr
|
|
1521
|
+
// const entityDesc = attributes['entity']
|
|
1522
|
+
// const entityIdDesc = attributes['entityId']
|
|
1523
|
+
// if (entityDesc && entityIdDesc && ((entityDesc.ref as string[])?.includes(attr))) {
|
|
1524
|
+
// // 特殊处理 entity 和 entityId 属性
|
|
1525
|
+
// }
|
|
1526
|
+
// continue;
|
|
1527
|
+
// }
|
|
1528
|
+
// 只能返回更新列的相关字段,所以直接assert
|
|
1529
|
+
(0, assert_1.default)(attributes.hasOwnProperty(attr), `RETURNING语法只能返回更新列的字段,但在实体 ${String(entity)} 中未找到原生属性「${attr}」的定义`);
|
|
1530
|
+
const dataType = attributes[attr].type;
|
|
1531
|
+
// 处理特殊类型的投影
|
|
1532
|
+
const columnExpr = this.translateAttrProjection(dataType, alias, attr);
|
|
1533
|
+
// RETURNING 需要明确的别名(不带表前缀)
|
|
1534
|
+
columns.push(`${columnExpr} AS "${attr}"`);
|
|
1513
1535
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1536
|
+
if (columns.length === 0) {
|
|
1537
|
+
throw new Error(`No valid columns in RETURNING clause for entity ${String(entity)}`);
|
|
1538
|
+
}
|
|
1539
|
+
return columns.join(', ');
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* 验证操作参数的合法性
|
|
1543
|
+
*/
|
|
1544
|
+
validateOperationParams(sorter, indexFrom, count) {
|
|
1545
|
+
(0, assert_1.default)(!sorter, '当前update/remove不支持sorter行为');
|
|
1546
|
+
// 对于limit,如果有indexFrom则一定有count
|
|
1547
|
+
if (typeof indexFrom === 'number') {
|
|
1548
|
+
(0, assert_1.default)(typeof count === 'number' && count > 0);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* 添加RETURNING子句
|
|
1553
|
+
*/
|
|
1554
|
+
appendReturningClause(sql, entity, mainAlias, option) {
|
|
1555
|
+
if (option?.returning) {
|
|
1556
|
+
const returningClause = this.buildReturningClause(entity, mainAlias, option.returning);
|
|
1557
|
+
return `${sql} RETURNING ${returningClause}`;
|
|
1529
1558
|
}
|
|
1530
1559
|
return sql;
|
|
1531
1560
|
}
|
|
1532
1561
|
translateRemove(entity, operation, option) {
|
|
1533
1562
|
const { data, filter, sorter, indexFrom, count } = operation;
|
|
1534
|
-
(0, assert_1.default)(!sorter, '当前remove不支持sorter行为');
|
|
1535
|
-
// 使用结构化的JOIN分析
|
|
1536
|
-
const { aliasDict, filterRefAlias, mainTable, mainAlias, joinInfos, currentNumber } = this.analyzeJoinStructured(entity, { filter, sorter });
|
|
1537
|
-
// 构建过滤条件
|
|
1538
|
-
const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, { includedDeleted: option?.includedDeleted });
|
|
1539
1563
|
const { attributes } = this.schema[entity];
|
|
1564
|
+
// 参数验证
|
|
1565
|
+
this.validateOperationParams(sorter, indexFrom, count);
|
|
1566
|
+
const mainTable = this.getStorageName(entity);
|
|
1567
|
+
const mainAlias = `${entity}_1`;
|
|
1568
|
+
// 构建子查询
|
|
1569
|
+
const subqueryOperation = {
|
|
1570
|
+
data: { [types_1.PrimaryKeyAttribute]: 1 },
|
|
1571
|
+
filter: filter,
|
|
1572
|
+
indexFrom: indexFrom,
|
|
1573
|
+
count: count
|
|
1574
|
+
};
|
|
1575
|
+
const subquery = this.translateSelect(entity, subqueryOperation, { includedDeleted: option?.includedDeleted });
|
|
1576
|
+
let sql;
|
|
1540
1577
|
if (option?.deletePhysically) {
|
|
1541
1578
|
// 物理删除
|
|
1542
1579
|
(0, assert_1.default)((0, lodash_1.difference)(Object.keys(data), [types_1.UpdateAtAttribute, types_1.DeleteAtAttribute]).length === 0);
|
|
1543
|
-
|
|
1544
|
-
if (joinInfos.length === 0) {
|
|
1545
|
-
// 单表删除
|
|
1546
|
-
sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
|
|
1547
|
-
if (filterText) {
|
|
1548
|
-
sql += ` WHERE ${filterText}`;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
else {
|
|
1552
|
-
// 多表删除 - PostgreSQL语法: DELETE FROM main USING other_tables WHERE join_conditions AND filter
|
|
1553
|
-
sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
|
|
1554
|
-
// USING子句包含所有JOIN的表
|
|
1555
|
-
const usingTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
|
|
1556
|
-
sql += ` USING ${usingTables}`;
|
|
1557
|
-
// WHERE子句包含JOIN条件和过滤条件
|
|
1558
|
-
const joinConditions = this.buildJoinConditions(joinInfos);
|
|
1559
|
-
let whereClause = joinConditions;
|
|
1560
|
-
if (filterText) {
|
|
1561
|
-
whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
|
|
1562
|
-
}
|
|
1563
|
-
if (whereClause) {
|
|
1564
|
-
sql += ` WHERE ${whereClause}`;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
return sql;
|
|
1580
|
+
sql = `DELETE FROM "${mainTable}" AS "${mainAlias}" WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
|
|
1568
1581
|
}
|
|
1569
1582
|
else {
|
|
1570
|
-
//
|
|
1583
|
+
// 软删除- 实际是 UPDATE
|
|
1571
1584
|
const setClauses = [];
|
|
1572
1585
|
for (const attr in data) {
|
|
1573
1586
|
(0, assert_1.default)([types_1.TriggerDataAttribute, types_1.TriggerUuidAttribute, types_1.DeleteAtAttribute, types_1.UpdateAtAttribute].includes(attr));
|
|
@@ -1575,164 +1588,10 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1575
1588
|
setClauses.push(`"${attr}" = ${value}`);
|
|
1576
1589
|
}
|
|
1577
1590
|
const setClause = setClauses.join(', ');
|
|
1578
|
-
|
|
1579
|
-
if (joinInfos.length === 0) {
|
|
1580
|
-
// 单表更新
|
|
1581
|
-
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
|
1582
|
-
if (filterText) {
|
|
1583
|
-
sql += ` WHERE ${filterText}`;
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
else {
|
|
1587
|
-
// 多表更新
|
|
1588
|
-
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
|
1589
|
-
const fromTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
|
|
1590
|
-
sql += ` FROM ${fromTables}`;
|
|
1591
|
-
const joinConditions = this.buildJoinConditions(joinInfos);
|
|
1592
|
-
let whereClause = joinConditions;
|
|
1593
|
-
if (filterText) {
|
|
1594
|
-
whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
|
|
1595
|
-
}
|
|
1596
|
-
if (whereClause) {
|
|
1597
|
-
sql += ` WHERE ${whereClause}`;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
return sql;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* PostgreSQL专用的结构化JOIN分析
|
|
1605
|
-
* 返回结构化的JOIN信息,而不是拼接好的FROM字符串
|
|
1606
|
-
*/
|
|
1607
|
-
analyzeJoinStructured(entity, { filter, sorter }, initialNumber) {
|
|
1608
|
-
const { schema } = this;
|
|
1609
|
-
let number = initialNumber || 1;
|
|
1610
|
-
const filterRefAlias = {};
|
|
1611
|
-
const mainAlias = `${entity}_${number++}`;
|
|
1612
|
-
const mainTable = this.getStorageName(entity);
|
|
1613
|
-
const aliasDict = {
|
|
1614
|
-
'./': mainAlias,
|
|
1615
|
-
};
|
|
1616
|
-
const joinInfos = [];
|
|
1617
|
-
const analyzeFilterNode = ({ node, path, entityName, alias }) => {
|
|
1618
|
-
Object.keys(node).forEach((op) => {
|
|
1619
|
-
if (['$and', '$or'].includes(op)) {
|
|
1620
|
-
node[op].forEach((subNode) => analyzeFilterNode({
|
|
1621
|
-
node: subNode,
|
|
1622
|
-
path,
|
|
1623
|
-
entityName,
|
|
1624
|
-
alias,
|
|
1625
|
-
}));
|
|
1626
|
-
}
|
|
1627
|
-
else if (['$not'].includes(op)) {
|
|
1628
|
-
analyzeFilterNode({
|
|
1629
|
-
node: node[op],
|
|
1630
|
-
path,
|
|
1631
|
-
entityName,
|
|
1632
|
-
alias,
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
else if (['$text'].includes(op)) {
|
|
1636
|
-
// 全文搜索,不需要JOIN
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
const rel = (0, relation_1.judgeRelation)(this.schema, entityName, op);
|
|
1640
|
-
if (typeof rel === 'string') {
|
|
1641
|
-
const pathAttr = `${path}${op}/`;
|
|
1642
|
-
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
|
1643
|
-
const alias2 = `${rel}_${number++}`;
|
|
1644
|
-
aliasDict[pathAttr] = alias2;
|
|
1645
|
-
joinInfos.push({
|
|
1646
|
-
table: this.getStorageName(rel),
|
|
1647
|
-
alias: alias2,
|
|
1648
|
-
leftAlias: alias,
|
|
1649
|
-
leftKey: op + 'Id',
|
|
1650
|
-
rightKey: 'id',
|
|
1651
|
-
});
|
|
1652
|
-
analyzeFilterNode({
|
|
1653
|
-
node: node[op],
|
|
1654
|
-
path: pathAttr,
|
|
1655
|
-
entityName: rel,
|
|
1656
|
-
alias: alias2,
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
else {
|
|
1660
|
-
analyzeFilterNode({
|
|
1661
|
-
node: node[op],
|
|
1662
|
-
path: pathAttr,
|
|
1663
|
-
entityName: rel,
|
|
1664
|
-
alias: aliasDict[pathAttr],
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
else if (rel === 2) {
|
|
1669
|
-
const pathAttr = `${path}${op}/`;
|
|
1670
|
-
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
|
1671
|
-
const alias2 = `${op}_${number++}`;
|
|
1672
|
-
aliasDict[pathAttr] = alias2;
|
|
1673
|
-
joinInfos.push({
|
|
1674
|
-
table: this.getStorageName(op),
|
|
1675
|
-
alias: alias2,
|
|
1676
|
-
leftAlias: alias,
|
|
1677
|
-
leftKey: 'entityId',
|
|
1678
|
-
rightKey: 'id',
|
|
1679
|
-
extraCondition: `"${alias}"."entity" = '${op}'`,
|
|
1680
|
-
});
|
|
1681
|
-
analyzeFilterNode({
|
|
1682
|
-
node: node[op],
|
|
1683
|
-
path: pathAttr,
|
|
1684
|
-
entityName: op,
|
|
1685
|
-
alias: alias2,
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
else {
|
|
1689
|
-
analyzeFilterNode({
|
|
1690
|
-
node: node[op],
|
|
1691
|
-
path: pathAttr,
|
|
1692
|
-
entityName: op,
|
|
1693
|
-
alias: aliasDict[pathAttr],
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
});
|
|
1699
|
-
if (node['#id']) {
|
|
1700
|
-
(0, assert_1.default)(!filterRefAlias[node['#id']]);
|
|
1701
|
-
filterRefAlias[node['#id']] = [alias, entityName];
|
|
1702
|
-
}
|
|
1703
|
-
};
|
|
1704
|
-
if (filter) {
|
|
1705
|
-
analyzeFilterNode({
|
|
1706
|
-
node: filter,
|
|
1707
|
-
path: './',
|
|
1708
|
-
entityName: entity,
|
|
1709
|
-
alias: mainAlias,
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
// TODO: sorter的分析类似,这里省略(UPDATE/DELETE通常不需要sorter)
|
|
1713
|
-
(0, assert_1.default)(!sorter, '当前analyzeJoinStructured不支持sorter行为');
|
|
1714
|
-
return {
|
|
1715
|
-
aliasDict,
|
|
1716
|
-
filterRefAlias,
|
|
1717
|
-
mainTable,
|
|
1718
|
-
mainAlias,
|
|
1719
|
-
joinInfos,
|
|
1720
|
-
currentNumber: number,
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
/**
|
|
1724
|
-
* 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
|
|
1725
|
-
*/
|
|
1726
|
-
buildJoinConditions(joinInfos) {
|
|
1727
|
-
const conditions = [];
|
|
1728
|
-
for (const join of joinInfos) {
|
|
1729
|
-
let condition = `"${join.leftAlias}"."${join.leftKey}" = "${join.alias}"."${join.rightKey}"`;
|
|
1730
|
-
if (join.extraCondition) {
|
|
1731
|
-
condition = `(${condition} AND ${join.extraCondition})`;
|
|
1732
|
-
}
|
|
1733
|
-
conditions.push(condition);
|
|
1591
|
+
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause} WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
|
|
1734
1592
|
}
|
|
1735
|
-
|
|
1593
|
+
// 添加 RETURNING 子句
|
|
1594
|
+
return this.appendReturningClause(sql, entity, mainAlias, option);
|
|
1736
1595
|
}
|
|
1737
1596
|
/**
|
|
1738
1597
|
* 生成 PostgreSQL UPSERT 语句
|
|
@@ -1766,5 +1625,401 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1766
1625
|
// 这个方法不应该被直接调用了,因为translateRemove已经重写
|
|
1767
1626
|
throw new Error('populateRemoveStmt should not be called directly in PostgreSQL. Use translateRemove instead.');
|
|
1768
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* 将 PostgreSQL 返回的 Type 回译成 oak 的类型,是 populateDataTypeDef 的反函数
|
|
1630
|
+
* @param type PostgreSQL 类型字符串
|
|
1631
|
+
*/
|
|
1632
|
+
reTranslateToAttribute(type) {
|
|
1633
|
+
// 处理带长度的类型:character varying(255), character(10)
|
|
1634
|
+
const varcharMatch = /^character varying\((\d+)\)$/.exec(type);
|
|
1635
|
+
if (varcharMatch) {
|
|
1636
|
+
return {
|
|
1637
|
+
type: 'varchar',
|
|
1638
|
+
params: {
|
|
1639
|
+
length: parseInt(varcharMatch[1], 10),
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const charMatch = /^character\((\d+)\)$/.exec(type);
|
|
1644
|
+
if (charMatch) {
|
|
1645
|
+
return {
|
|
1646
|
+
type: 'char',
|
|
1647
|
+
params: {
|
|
1648
|
+
length: parseInt(charMatch[1], 10),
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
// 处理带精度和小数位的类型:numeric(10,2)
|
|
1653
|
+
const numericWithScaleMatch = /^numeric\((\d+),(\d+)\)$/.exec(type);
|
|
1654
|
+
if (numericWithScaleMatch) {
|
|
1655
|
+
return {
|
|
1656
|
+
type: 'decimal',
|
|
1657
|
+
params: {
|
|
1658
|
+
precision: parseInt(numericWithScaleMatch[1], 10),
|
|
1659
|
+
scale: parseInt(numericWithScaleMatch[2], 10),
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
// 处理只带精度的类型:numeric(10), timestamp(6)
|
|
1664
|
+
const numericMatch = /^numeric\((\d+)\)$/.exec(type);
|
|
1665
|
+
if (numericMatch) {
|
|
1666
|
+
return {
|
|
1667
|
+
type: 'decimal',
|
|
1668
|
+
params: {
|
|
1669
|
+
precision: parseInt(numericMatch[1], 10),
|
|
1670
|
+
scale: 0,
|
|
1671
|
+
},
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
const timestampMatch = /^timestamp\((\d+)\) without time zone$/.exec(type);
|
|
1675
|
+
if (timestampMatch) {
|
|
1676
|
+
return {
|
|
1677
|
+
type: 'timestamp',
|
|
1678
|
+
params: {
|
|
1679
|
+
precision: parseInt(timestampMatch[1], 10),
|
|
1680
|
+
},
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const timeMatch = /^time\((\d+)\) without time zone$/.exec(type);
|
|
1684
|
+
if (timeMatch) {
|
|
1685
|
+
return {
|
|
1686
|
+
type: 'time',
|
|
1687
|
+
params: {
|
|
1688
|
+
precision: parseInt(timeMatch[1], 10),
|
|
1689
|
+
},
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
// PostgreSQL 类型映射到 oak 类型
|
|
1693
|
+
const typeMap = {
|
|
1694
|
+
'bigint': 'bigint',
|
|
1695
|
+
'integer': 'integer',
|
|
1696
|
+
'smallint': 'smallint',
|
|
1697
|
+
'real': 'real',
|
|
1698
|
+
'double precision': 'double precision',
|
|
1699
|
+
'boolean': 'boolean',
|
|
1700
|
+
'text': 'text',
|
|
1701
|
+
'jsonb': 'object',
|
|
1702
|
+
'json': 'object',
|
|
1703
|
+
'bytea': 'bytea',
|
|
1704
|
+
'character varying': 'varchar',
|
|
1705
|
+
'character': 'char',
|
|
1706
|
+
'timestamp without time zone': 'timestamp',
|
|
1707
|
+
'time without time zone': 'time',
|
|
1708
|
+
'date': 'date',
|
|
1709
|
+
'uuid': 'uuid',
|
|
1710
|
+
'geometry': 'geometry',
|
|
1711
|
+
'numeric': 'decimal',
|
|
1712
|
+
};
|
|
1713
|
+
const mappedType = typeMap[type];
|
|
1714
|
+
if (mappedType) {
|
|
1715
|
+
return { type: mappedType };
|
|
1716
|
+
}
|
|
1717
|
+
// 如果是用户定义的枚举类型,返回 enum(具体值需要额外查询)
|
|
1718
|
+
// 这里先返回基础类型,枚举值在 readSchema 中单独处理
|
|
1719
|
+
return { type: type };
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* 从 PostgreSQL 数据库读取当前的 schema 结构
|
|
1723
|
+
*/
|
|
1724
|
+
async readSchema(execFn) {
|
|
1725
|
+
const result = {};
|
|
1726
|
+
// 1. 获取所有表
|
|
1727
|
+
const tablesSql = `
|
|
1728
|
+
SELECT tablename
|
|
1729
|
+
FROM pg_tables
|
|
1730
|
+
WHERE schemaname = 'public'
|
|
1731
|
+
ORDER BY tablename;
|
|
1732
|
+
`;
|
|
1733
|
+
const [tablesResult] = await execFn(tablesSql);
|
|
1734
|
+
for (const tableRow of tablesResult) {
|
|
1735
|
+
const tableName = tableRow.tablename;
|
|
1736
|
+
// 2. 获取表的列信息
|
|
1737
|
+
const columnsSql = `
|
|
1738
|
+
SELECT
|
|
1739
|
+
column_name,
|
|
1740
|
+
data_type,
|
|
1741
|
+
character_maximum_length,
|
|
1742
|
+
numeric_precision,
|
|
1743
|
+
numeric_scale,
|
|
1744
|
+
is_nullable,
|
|
1745
|
+
column_default,
|
|
1746
|
+
udt_name
|
|
1747
|
+
FROM information_schema.columns
|
|
1748
|
+
WHERE table_schema = 'public'
|
|
1749
|
+
AND table_name = '${tableName}'
|
|
1750
|
+
ORDER BY ordinal_position;
|
|
1751
|
+
`;
|
|
1752
|
+
const [columnsResult] = await execFn(columnsSql);
|
|
1753
|
+
const attributes = {};
|
|
1754
|
+
for (const col of columnsResult) {
|
|
1755
|
+
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;
|
|
1756
|
+
let attr;
|
|
1757
|
+
// 处理用户定义类型(枚举)
|
|
1758
|
+
if (dataType === 'USER-DEFINED') {
|
|
1759
|
+
const enumSql = `
|
|
1760
|
+
SELECT e.enumlabel
|
|
1761
|
+
FROM pg_type t
|
|
1762
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
1763
|
+
WHERE t.typname = '${udtName}'
|
|
1764
|
+
ORDER BY e.enumsortorder;
|
|
1765
|
+
`;
|
|
1766
|
+
const [enumResult] = await execFn(enumSql);
|
|
1767
|
+
const enumeration = enumResult.map((r) => r.enumlabel);
|
|
1768
|
+
attr = {
|
|
1769
|
+
type: 'enum',
|
|
1770
|
+
enumeration,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
else {
|
|
1774
|
+
// 构建完整的类型字符串
|
|
1775
|
+
let fullType = dataType;
|
|
1776
|
+
const integerTypes = ['bigint', 'integer', 'smallint', 'serial', 'bigserial', 'smallserial'];
|
|
1777
|
+
if (maxLength && !integerTypes.includes(dataType)) {
|
|
1778
|
+
fullType = `${dataType}(${maxLength})`;
|
|
1779
|
+
}
|
|
1780
|
+
else if (precision !== null && scale !== null && !integerTypes.includes(dataType)) {
|
|
1781
|
+
fullType = `${dataType}(${precision},${scale})`;
|
|
1782
|
+
}
|
|
1783
|
+
else if (precision !== null && !integerTypes.includes(dataType)) {
|
|
1784
|
+
fullType = `${dataType}(${precision})`;
|
|
1785
|
+
}
|
|
1786
|
+
attr = this.reTranslateToAttribute(fullType);
|
|
1787
|
+
}
|
|
1788
|
+
// ========== 类型还原逻辑 ==========
|
|
1789
|
+
// 框架将某些语义类型存储为 bigint,需要根据列名还原
|
|
1790
|
+
if (attr.type === 'bigint') {
|
|
1791
|
+
// 1. 检查是否是序列列
|
|
1792
|
+
if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) {
|
|
1793
|
+
attr.type = 'sequence';
|
|
1794
|
+
attr.sequenceStart = 10000; // 默认起始值
|
|
1795
|
+
}
|
|
1796
|
+
// 2. 检查是否是时间戳列
|
|
1797
|
+
else if (['$$createAt$$', '$$updateAt$$', '$$deleteAt$$'].includes(colName)) {
|
|
1798
|
+
attr.type = 'datetime';
|
|
1799
|
+
}
|
|
1800
|
+
// 3. 检查其他可能的时间类型列(根据命名约定)
|
|
1801
|
+
else if (colName.endsWith('At') || colName.endsWith('Time')) {
|
|
1802
|
+
// 可选:根据业务约定判断是否应该是 datetime
|
|
1803
|
+
// 这里保守处理,只转换框架标准字段
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
// 处理约束 - 只在为 true 时添加
|
|
1807
|
+
if (isNullable === 'NO') {
|
|
1808
|
+
attr.notNull = true;
|
|
1809
|
+
}
|
|
1810
|
+
// 处理默认值
|
|
1811
|
+
if (defaultValue && !defaultValue.includes('nextval')) {
|
|
1812
|
+
let cleanDefault = defaultValue.replace(/::[a-z]+/gi, '').replace(/'/g, '');
|
|
1813
|
+
if (cleanDefault === 'true') {
|
|
1814
|
+
attr.default = true;
|
|
1815
|
+
}
|
|
1816
|
+
else if (cleanDefault === 'false') {
|
|
1817
|
+
attr.default = false;
|
|
1818
|
+
}
|
|
1819
|
+
else if (!isNaN(Number(cleanDefault))) {
|
|
1820
|
+
attr.default = Number(cleanDefault);
|
|
1821
|
+
}
|
|
1822
|
+
else if (cleanDefault !== '') {
|
|
1823
|
+
attr.default = cleanDefault;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
// 检查唯一约束
|
|
1827
|
+
const uniqueSql = `
|
|
1828
|
+
SELECT COUNT(*) as cnt
|
|
1829
|
+
FROM pg_index ix
|
|
1830
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
1831
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
1832
|
+
WHERE t.relname = '${tableName}'
|
|
1833
|
+
AND a.attname = '${colName}'
|
|
1834
|
+
AND ix.indisunique = true
|
|
1835
|
+
AND NOT ix.indisprimary
|
|
1836
|
+
AND array_length(ix.indkey, 1) = 1;
|
|
1837
|
+
`;
|
|
1838
|
+
const [uniqueResult] = await execFn(uniqueSql);
|
|
1839
|
+
const uniqueCount = parseInt(uniqueResult[0]?.cnt || '0', 10);
|
|
1840
|
+
if (uniqueCount > 0) {
|
|
1841
|
+
attr.unique = true;
|
|
1842
|
+
}
|
|
1843
|
+
attributes[colName] = attr;
|
|
1844
|
+
}
|
|
1845
|
+
// 3. 获取索引信息
|
|
1846
|
+
const indexesSql = `
|
|
1847
|
+
SELECT
|
|
1848
|
+
i.relname as index_name,
|
|
1849
|
+
ix.indisunique as is_unique,
|
|
1850
|
+
am.amname as index_type,
|
|
1851
|
+
pg_get_indexdef(ix.indexrelid) as index_def
|
|
1852
|
+
FROM pg_class t
|
|
1853
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
1854
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
1855
|
+
JOIN pg_am am ON i.relam = am.oid
|
|
1856
|
+
WHERE t.relname = '${tableName}'
|
|
1857
|
+
AND t.relkind = 'r'
|
|
1858
|
+
AND i.relname NOT LIKE '%_pkey'
|
|
1859
|
+
AND NOT ix.indisprimary
|
|
1860
|
+
ORDER BY i.relname;
|
|
1861
|
+
`;
|
|
1862
|
+
const [indexesResult] = await execFn(indexesSql);
|
|
1863
|
+
if (indexesResult.length > 0) {
|
|
1864
|
+
const indexes = [];
|
|
1865
|
+
for (const row of indexesResult) {
|
|
1866
|
+
const { index_name: indexName, is_unique: isUnique, index_type: indexType, index_def: indexDef } = row;
|
|
1867
|
+
// 解析索引定义以获取列名和配置
|
|
1868
|
+
const index = {
|
|
1869
|
+
name: indexName,
|
|
1870
|
+
attributes: [],
|
|
1871
|
+
};
|
|
1872
|
+
// 解析索引定义字符串
|
|
1873
|
+
// 示例: CREATE INDEX "user_index_fulltext_chinese" ON public."user" USING gin (to_tsvector('chinese'::regconfig, (COALESCE(name, ''::text) || ' '::text) || COALESCE(nickname, ''::text)))
|
|
1874
|
+
if (indexType === 'gin' && indexDef.includes('to_tsvector')) {
|
|
1875
|
+
// 全文索引
|
|
1876
|
+
index.config = { type: 'fulltext' };
|
|
1877
|
+
// 提取 tsConfig
|
|
1878
|
+
const tsConfigMatch = indexDef.match(/to_tsvector\('([^']+)'/);
|
|
1879
|
+
if (tsConfigMatch) {
|
|
1880
|
+
const tsConfig = tsConfigMatch[1];
|
|
1881
|
+
index.config.tsConfig = tsConfig;
|
|
1882
|
+
}
|
|
1883
|
+
// 提取列名(从 COALESCE 中)
|
|
1884
|
+
const columnMatches = indexDef.matchAll(/COALESCE\("?([^",\s]+)"?/g);
|
|
1885
|
+
const columns = Array.from(columnMatches, m => m[1]);
|
|
1886
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
1887
|
+
// 处理多语言索引的情况:移除语言后缀
|
|
1888
|
+
// 例如: user_index_fulltext_chinese -> index_fulltext
|
|
1889
|
+
const nameParts = indexName.split('_');
|
|
1890
|
+
if (nameParts.length > 2) {
|
|
1891
|
+
const possibleLang = nameParts[nameParts.length - 1];
|
|
1892
|
+
// 如果最后一部分是语言代码,移除它
|
|
1893
|
+
if (['chinese', 'english', 'simple', 'german', 'french', 'spanish', 'russian', 'japanese'].includes(possibleLang)) {
|
|
1894
|
+
index.name = nameParts.slice(0, -1).join('_');
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
else if (indexType === 'gist') {
|
|
1899
|
+
// 空间索引
|
|
1900
|
+
index.config = { type: 'spatial' };
|
|
1901
|
+
// 提取列名
|
|
1902
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
1903
|
+
if (columnMatch) {
|
|
1904
|
+
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
|
1905
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
else if (indexType === 'hash') {
|
|
1909
|
+
// 哈希索引
|
|
1910
|
+
index.config = { type: 'hash' };
|
|
1911
|
+
// 提取列名
|
|
1912
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
1913
|
+
if (columnMatch) {
|
|
1914
|
+
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
|
1915
|
+
index.attributes = columns.map(col => ({ name: col }));
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
// B-tree 索引(默认)
|
|
1920
|
+
// 提取列名和排序方向
|
|
1921
|
+
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
|
1922
|
+
if (columnMatch) {
|
|
1923
|
+
const columnDefs = columnMatch[1].split(',');
|
|
1924
|
+
index.attributes = columnDefs.map(colDef => {
|
|
1925
|
+
const trimmed = colDef.trim().replace(/"/g, '');
|
|
1926
|
+
const parts = trimmed.split(/\s+/);
|
|
1927
|
+
const attr = { name: parts[0] };
|
|
1928
|
+
// 检查排序方向
|
|
1929
|
+
if (parts.includes('DESC')) {
|
|
1930
|
+
attr.direction = 'DESC';
|
|
1931
|
+
}
|
|
1932
|
+
else if (parts.includes('ASC')) {
|
|
1933
|
+
attr.direction = 'ASC';
|
|
1934
|
+
}
|
|
1935
|
+
return attr;
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
// 如果是唯一索引
|
|
1939
|
+
if (isUnique) {
|
|
1940
|
+
index.config = { unique: true };
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
// 移除表名前缀(如果存在)
|
|
1944
|
+
// 例如: user_index_fulltext -> index_fulltext
|
|
1945
|
+
if (index.name.startsWith(`${tableName}_`)) {
|
|
1946
|
+
index.name = index.name.substring(tableName.length + 1);
|
|
1947
|
+
}
|
|
1948
|
+
indexes.push(index);
|
|
1949
|
+
}
|
|
1950
|
+
Object.assign(result, {
|
|
1951
|
+
[tableName]: {
|
|
1952
|
+
attributes,
|
|
1953
|
+
indexes,
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
Object.assign(result, {
|
|
1959
|
+
[tableName]: {
|
|
1960
|
+
attributes,
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* 将属性定义转换为 PostgreSQL DDL 语句
|
|
1969
|
+
* @param attr 属性名
|
|
1970
|
+
* @param attrDef 属性定义
|
|
1971
|
+
*/
|
|
1972
|
+
translateAttributeDef(attr, attrDef) {
|
|
1973
|
+
let sql = `"${attr}" `;
|
|
1974
|
+
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
|
1975
|
+
// 处理序列类型(IDENTITY)
|
|
1976
|
+
if (type === 'sequence' || (typeof sequenceStart === 'number')) {
|
|
1977
|
+
sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart || 10000}) UNIQUE`;
|
|
1978
|
+
return sql;
|
|
1979
|
+
}
|
|
1980
|
+
// 处理枚举类型
|
|
1981
|
+
if (type === 'enum') {
|
|
1982
|
+
(0, assert_1.default)(enumeration, 'Enum type requires enumeration values');
|
|
1983
|
+
sql += `enum(${enumeration.map(v => `'${v}'`).join(',')})`;
|
|
1984
|
+
}
|
|
1985
|
+
else {
|
|
1986
|
+
sql += this.populateDataTypeDef(type, params, enumeration);
|
|
1987
|
+
}
|
|
1988
|
+
// NOT NULL 约束
|
|
1989
|
+
if (notNull || type === 'geometry') {
|
|
1990
|
+
sql += ' NOT NULL';
|
|
1991
|
+
}
|
|
1992
|
+
// UNIQUE 约束
|
|
1993
|
+
if (unique) {
|
|
1994
|
+
sql += ' UNIQUE';
|
|
1995
|
+
}
|
|
1996
|
+
// 默认值
|
|
1997
|
+
if (defaultValue !== undefined && !sequenceStart) {
|
|
1998
|
+
(0, assert_1.default)(type !== 'ref', 'ref type should not have default value');
|
|
1999
|
+
sql += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
|
|
2000
|
+
}
|
|
2001
|
+
// 主键
|
|
2002
|
+
if (attr === 'id') {
|
|
2003
|
+
sql += ' PRIMARY KEY';
|
|
2004
|
+
}
|
|
2005
|
+
return sql;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* 比较两个 SQL 语句是否等价(用于 schema diff)
|
|
2009
|
+
* 忽略空格、大小写等格式差异
|
|
2010
|
+
*/
|
|
2011
|
+
compareSql(sql1, sql2) {
|
|
2012
|
+
// 标准化 SQL:移除多余空格,统一大小写
|
|
2013
|
+
const normalize = (sql) => {
|
|
2014
|
+
return sql
|
|
2015
|
+
.replace(/\s+/g, ' ') // 多个空格合并为一个
|
|
2016
|
+
.replace(/\(\s+/g, '(') // 移除括号后的空格
|
|
2017
|
+
.replace(/\s+\)/g, ')') // 移除括号前的空格
|
|
2018
|
+
.replace(/,\s+/g, ',') // 移除逗号后的空格
|
|
2019
|
+
.trim()
|
|
2020
|
+
.toLowerCase();
|
|
2021
|
+
};
|
|
2022
|
+
return normalize(sql1) === normalize(sql2);
|
|
2023
|
+
}
|
|
1769
2024
|
}
|
|
1770
2025
|
exports.PostgreSQLTranslator = PostgreSQLTranslator;
|