2140 lines
88 KiB
JavaScript
2140 lines
88 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.PostgreSQLTranslator = void 0;
|
||
const tslib_1 = require("tslib");
|
||
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||
const util_1 = require("util");
|
||
const lodash_1 = require("lodash");
|
||
const types_1 = require("oak-domain/lib/types");
|
||
const sqlTranslator_1 = require("../sqlTranslator");
|
||
const relation_1 = require("oak-domain/lib/store/relation");
|
||
const GeoTypes = [
|
||
{
|
||
type: 'point',
|
||
name: "Point"
|
||
},
|
||
{
|
||
type: 'path',
|
||
name: "LineString",
|
||
element: 'point',
|
||
},
|
||
{
|
||
name: "MultiLineString",
|
||
element: "path",
|
||
multiple: true,
|
||
},
|
||
{
|
||
type: 'polygon',
|
||
name: "Polygon",
|
||
element: "path"
|
||
},
|
||
{
|
||
name: "MultiPoint",
|
||
element: "point",
|
||
multiple: true,
|
||
},
|
||
{
|
||
name: "MultiPolygon",
|
||
element: "polygon",
|
||
multiple: true,
|
||
}
|
||
];
|
||
/**
|
||
* 将 Geo 对象转换为 PostGIS 的 WKT 格式
|
||
*/
|
||
function transformGeoData(data) {
|
||
if (data instanceof Array) {
|
||
const element = data[0];
|
||
if (element instanceof Array) {
|
||
// GeometryCollection
|
||
return `ST_GeomFromText('GEOMETRYCOLLECTION(${data.map(ele => transformGeoData(ele).replace(/ST_GeomFromText\('|'\)/g, '')).join(',')})')`;
|
||
}
|
||
else {
|
||
// Multi 类型
|
||
const geoType = GeoTypes.find(ele => ele.type === element.type);
|
||
if (!geoType) {
|
||
throw new Error(`${element.type} is not supported in PostgreSQL`);
|
||
}
|
||
const multiGeoType = GeoTypes.find(ele => ele.element === geoType.type && ele.multiple);
|
||
if (!multiGeoType) {
|
||
throw new Error(`Multi type for ${element.type} not found`);
|
||
}
|
||
const innerWkt = data.map(ele => {
|
||
const wkt = transformGeoData(ele);
|
||
// 提取括号内的坐标部分
|
||
const match = wkt.match(/\(([^)]+)\)/);
|
||
return match ? match[0] : '';
|
||
}).join(',');
|
||
return `ST_GeomFromText('${multiGeoType.name.toUpperCase()}(${innerWkt})')`;
|
||
}
|
||
}
|
||
else {
|
||
const { type, coordinate } = data;
|
||
const geoType = GeoTypes.find(ele => ele.type === type);
|
||
if (!geoType) {
|
||
throw new Error(`${type} is not supported in PostgreSQL`);
|
||
}
|
||
const { element, name } = geoType;
|
||
if (!element) {
|
||
// Point: coordinate 是 [x, y]
|
||
return `ST_GeomFromText('POINT(${coordinate.join(' ')})')`;
|
||
}
|
||
if (type === 'path') {
|
||
// LineString: coordinate 是 [[x1,y1], [x2,y2], ...]
|
||
const points = coordinate
|
||
.map(p => p.join(' '))
|
||
.join(',');
|
||
return `ST_GeomFromText('LINESTRING(${points})')`;
|
||
}
|
||
if (type === 'polygon') {
|
||
// Polygon: coordinate 是 [[[x1,y1], [x2,y2], ...], [...]](外环和内环)
|
||
const rings = coordinate
|
||
.map(ring => `(${ring.map(p => p.join(' ')).join(',')})`)
|
||
.join(',');
|
||
return `ST_GeomFromText('POLYGON(${rings})')`;
|
||
}
|
||
throw new Error(`Unsupported geometry type: ${type}`);
|
||
}
|
||
}
|
||
class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
||
// 生成 enum 类型名称
|
||
getEnumTypeName(entity, attr) {
|
||
return `${entity}_${attr}_enum`.toLowerCase();
|
||
}
|
||
/**
|
||
* 将 MySQL 风格的 JSON 路径转换为 PostgreSQL 路径数组格式
|
||
* 例如: ".foo.bar[0].baz" -> '{foo,bar,0,baz}'
|
||
*/
|
||
convertJsonPath(mysqlPath) {
|
||
if (!mysqlPath || mysqlPath === '') {
|
||
return '';
|
||
}
|
||
// 移除开头的点
|
||
let path = mysqlPath.startsWith('.') ? mysqlPath.slice(1) : mysqlPath;
|
||
// 解析路径组件
|
||
const components = [];
|
||
let current = '';
|
||
let i = 0;
|
||
while (i < path.length) {
|
||
const char = path[i];
|
||
if (char === '.') {
|
||
if (current) {
|
||
components.push(current);
|
||
current = '';
|
||
}
|
||
i++;
|
||
}
|
||
else if (char === '[') {
|
||
if (current) {
|
||
components.push(current);
|
||
current = '';
|
||
}
|
||
// 找到对应的 ]
|
||
const endBracket = path.indexOf(']', i);
|
||
if (endBracket === -1) {
|
||
throw new Error(`Invalid JSON path: unmatched [ at position ${i}`);
|
||
}
|
||
const index = path.slice(i + 1, endBracket);
|
||
components.push(index);
|
||
i = endBracket + 1;
|
||
}
|
||
else {
|
||
current += char;
|
||
i++;
|
||
}
|
||
}
|
||
if (current) {
|
||
components.push(current);
|
||
}
|
||
return `'{${components.join(',')}}'`;
|
||
}
|
||
/**
|
||
* 生成 PostgreSQL JSON 访问表达式
|
||
* @param column 列名(包含别名)
|
||
* @param path JSON 路径
|
||
* @param asText 是否返回文本(使用 #>> 而不是 #>)
|
||
*/
|
||
buildJsonAccessor(column, path, asText = true) {
|
||
if (!path || path === '') {
|
||
return column;
|
||
}
|
||
const pgPath = this.convertJsonPath(path);
|
||
const operator = asText ? '#>>' : '#>';
|
||
return `${column}${operator}${pgPath}`;
|
||
}
|
||
getDefaultSelectFilter(alias, option) {
|
||
if (option?.includedDeleted) {
|
||
return '';
|
||
}
|
||
return ` ("${alias}"."$$deleteAt$$" is null)`;
|
||
}
|
||
makeUpSchema() {
|
||
for (const entity in this.schema) {
|
||
const { attributes, indexes } = this.schema[entity];
|
||
// 非特殊索引自动添加 $$deleteAt$$
|
||
for (const index of indexes || []) {
|
||
if (index.config?.type) {
|
||
continue;
|
||
}
|
||
const indexAttrNames = index.attributes.map(attr => attr.name);
|
||
if (!indexAttrNames.includes('$$deleteAt$$')) {
|
||
index.attributes.push({ name: '$$deleteAt$$' });
|
||
}
|
||
}
|
||
const geoIndexes = [];
|
||
for (const attr in attributes) {
|
||
if (attributes[attr].type === 'geometry') {
|
||
const geoIndex = indexes?.find((idx) => idx.config?.type === 'spatial' && idx.attributes.find((attrDef) => attrDef.name === attr));
|
||
if (!geoIndex) {
|
||
geoIndexes.push({
|
||
name: `${entity}_geo_${attr}`,
|
||
attributes: [{
|
||
name: attr,
|
||
}],
|
||
config: {
|
||
type: 'spatial',
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
if (geoIndexes.length > 0) {
|
||
if (indexes) {
|
||
indexes.push(...geoIndexes);
|
||
}
|
||
else {
|
||
(0, lodash_1.assign)(this.schema[entity], {
|
||
indexes: geoIndexes,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
constructor(schema) {
|
||
super(schema);
|
||
// PostgreSQL为geometry属性默认创建索引
|
||
this.makeUpSchema();
|
||
}
|
||
static supportedDataTypes = [
|
||
// numeric types
|
||
"smallint",
|
||
"integer",
|
||
"bigint",
|
||
"decimal",
|
||
"numeric",
|
||
"real",
|
||
"double precision",
|
||
// TODO: 下面三种类型暂不支持(在oak-domain中未定义)
|
||
// "serial",
|
||
// "bigserial",
|
||
// "smallserial",
|
||
// boolean
|
||
"boolean",
|
||
// date and time types
|
||
"date",
|
||
"time",
|
||
"timestamp",
|
||
"timestamptz",
|
||
"interval",
|
||
// string types
|
||
"char",
|
||
"varchar",
|
||
"text",
|
||
// binary
|
||
"bytea",
|
||
// json data types
|
||
"json",
|
||
"jsonb",
|
||
// uuid
|
||
"uuid",
|
||
// spatial data types (PostGIS)
|
||
"geometry",
|
||
"geography",
|
||
"point",
|
||
"line",
|
||
"lseg",
|
||
"box",
|
||
"path",
|
||
"polygon",
|
||
"circle"
|
||
];
|
||
static spatialTypes = [
|
||
"geometry",
|
||
"geography",
|
||
"point",
|
||
"line",
|
||
"lseg",
|
||
"box",
|
||
"path",
|
||
"polygon",
|
||
"circle"
|
||
];
|
||
static withLengthDataTypes = [
|
||
"char",
|
||
"varchar",
|
||
"bit",
|
||
"varbit"
|
||
];
|
||
static withPrecisionDataTypes = [
|
||
"decimal",
|
||
"numeric",
|
||
"time",
|
||
"timestamp",
|
||
"timestamptz"
|
||
];
|
||
static withScaleDataTypes = [
|
||
"decimal",
|
||
"numeric"
|
||
];
|
||
// PostgreSQL 不支持 unsigned,移除此列表
|
||
// static unsignedAndZerofillTypes - 删除
|
||
// PostgreSQL 不使用 width 概念
|
||
// static withWidthDataTypes - 删除
|
||
static dataTypeDefaults = {
|
||
"varchar": { length: 255 },
|
||
"char": { length: 1 },
|
||
"decimal": { precision: 10, scale: 0 },
|
||
"numeric": { precision: 10, scale: 0 },
|
||
"time": { precision: 6 },
|
||
"timestamp": { precision: 6 },
|
||
"timestamptz": { precision: 6 },
|
||
"bit": { length: 1 },
|
||
"varbit": { length: 255 }
|
||
};
|
||
maxAliasLength = 63;
|
||
populateDataTypeDef(type, params, enumeration) {
|
||
// 时间类型 - 框架内部用 bigint 存储 Unix 时间戳
|
||
if (['date', 'datetime', 'time', 'sequence'].includes(type)) {
|
||
return 'bigint';
|
||
}
|
||
// JSON 类型 - PostgreSQL 推荐使用 jsonb
|
||
if (['object', 'array'].includes(type)) {
|
||
return 'jsonb';
|
||
}
|
||
// 文本类型
|
||
if (['image', 'function'].includes(type)) {
|
||
return 'text';
|
||
}
|
||
// 引用类型 - UUID 字符串
|
||
if (type === 'ref') {
|
||
return 'char(36)';
|
||
}
|
||
// 金额类型
|
||
if (type === 'money') {
|
||
return 'bigint';
|
||
}
|
||
// 布尔类型
|
||
if (type === 'bool' || type === 'boolean') {
|
||
return 'boolean';
|
||
}
|
||
// 枚举类型 - PostgreSQL 需要使用预创建的 TYPE
|
||
// 这里返回类型名称,实际创建在 translateCreateEntity 中处理
|
||
if (type === 'enum') {
|
||
(0, assert_1.default)(enumeration);
|
||
// 返回一个占位符,实际类型名会在创建表时确定
|
||
return '__ENUM_PLACEHOLDER__';
|
||
}
|
||
// int 类型映射
|
||
if (type === 'int' || type === 'integer') {
|
||
if (params?.width) {
|
||
const { width } = params;
|
||
if (width <= 2) {
|
||
return 'smallint';
|
||
}
|
||
else if (width <= 4) {
|
||
return 'integer';
|
||
}
|
||
else {
|
||
return 'bigint';
|
||
}
|
||
}
|
||
return 'integer';
|
||
}
|
||
// MySQL 特有类型映射到 PostgreSQL
|
||
if (type === 'tinyint') {
|
||
return 'smallint';
|
||
}
|
||
if (type === 'mediumint') {
|
||
return 'integer';
|
||
}
|
||
if (type === 'double') {
|
||
return 'double precision';
|
||
}
|
||
if (type === 'float') {
|
||
return 'real';
|
||
}
|
||
if (type === 'datetime') {
|
||
return 'timestamp';
|
||
}
|
||
if (type === 'blob' || type === 'tinyblob' || type === 'mediumblob' || type === 'longblob') {
|
||
return 'bytea';
|
||
}
|
||
if (type === 'tinytext' || type === 'mediumtext' || type === 'longtext') {
|
||
return 'text';
|
||
}
|
||
// 带长度的类型
|
||
if (PostgreSQLTranslator.withLengthDataTypes.includes(type)) {
|
||
const defaults = PostgreSQLTranslator.dataTypeDefaults[type];
|
||
const length = params?.length ?? defaults?.length;
|
||
if (length) {
|
||
return `${type}(${length})`;
|
||
}
|
||
return type;
|
||
}
|
||
// 带精度的类型
|
||
if (PostgreSQLTranslator.withPrecisionDataTypes.includes(type)) {
|
||
const defaults = PostgreSQLTranslator.dataTypeDefaults[type];
|
||
const precision = params?.precision ?? defaults?.precision;
|
||
const scale = params?.scale ?? defaults?.scale;
|
||
if (precision !== undefined) {
|
||
if (scale !== undefined && PostgreSQLTranslator.withScaleDataTypes.includes(type)) {
|
||
return `${type}(${precision}, ${scale})`;
|
||
}
|
||
return `${type}(${precision})`;
|
||
}
|
||
return type;
|
||
}
|
||
// geometry 类型 - 需要 PostGIS 扩展
|
||
if (type === 'geometry') {
|
||
return 'geometry';
|
||
}
|
||
return type;
|
||
}
|
||
/**
|
||
* PostgreSQL 字符串值转义
|
||
* 防御SQL注入
|
||
*/
|
||
escapeStringValue(value) {
|
||
if (value === null || value === undefined) {
|
||
return 'NULL';
|
||
}
|
||
// PostgreSQL 标准 SQL 转义
|
||
// 1. 单引号转义为两个单引号
|
||
// 2. 反斜杠在 standard_conforming_strings=on(默认)时不需要特殊处理
|
||
const escaped = String(value).replace(/'/g, "''");
|
||
return `'${escaped}'`;
|
||
}
|
||
/**
|
||
* LIKE 模式转义
|
||
* 转义 LIKE 语句中的特殊字符
|
||
*/
|
||
escapeLikePattern(value) {
|
||
// 先转义 LIKE 的特殊字符
|
||
const escaped = String(value)
|
||
.replace(/\\/g, '\\\\') // 反斜杠必须先处理
|
||
.replace(/%/g, '\\%') // 百分号
|
||
.replace(/_/g, '\\_'); // 下划线
|
||
// 再进行字符串值转义
|
||
return escaped.replace(/'/g, "''");
|
||
}
|
||
/**
|
||
* PostgreSQL 标识符转义
|
||
* 用于表名、列名等
|
||
*/
|
||
escapeIdentifier(identifier) {
|
||
// 标识符中的双引号转义为两个双引号
|
||
const escaped = String(identifier).replace(/"/g, '""');
|
||
return `"${escaped}"`;
|
||
}
|
||
/**
|
||
* tsquery 搜索词转义
|
||
*/
|
||
escapeTsQueryValue(value) {
|
||
// 移除或转义 tsquery 的特殊字符
|
||
return String(value)
|
||
.replace(/'/g, "''") // SQL 单引号
|
||
.replace(/[&|!():<>\\]/g, ' ') // tsquery 特殊字符替换为空格
|
||
.replace(/\s+/g, ' ') // 合并多个空格
|
||
.trim();
|
||
}
|
||
quoteIdentifier(identifier) {
|
||
return `"${identifier}"`;
|
||
}
|
||
translateAttrProjection(dataType, alias, attr) {
|
||
switch (dataType) {
|
||
case 'geometry': {
|
||
return ` st_astext("${alias}"."${attr}")`;
|
||
}
|
||
default: {
|
||
return ` "${alias}"."${attr}"`;
|
||
}
|
||
}
|
||
}
|
||
translateObjectPredicate(predicate, alias, attr) {
|
||
const columnRef = `"${alias}"."${attr}"`;
|
||
const translateInner = (o, p) => {
|
||
let stmt2 = '';
|
||
if (o instanceof Array) {
|
||
o.forEach((ele, idx) => {
|
||
if (ele !== undefined && ele !== null) {
|
||
const part = translateInner(ele, `${p}[${idx}]`);
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
stmt2 += part;
|
||
}
|
||
});
|
||
}
|
||
else if (typeof o === 'object' && o !== null) {
|
||
for (const attr2 in o) {
|
||
if (attr2 === '$and') {
|
||
const andParts = [];
|
||
o[attr2].forEach((ele) => {
|
||
const part = translateInner(ele, p);
|
||
if (part) {
|
||
andParts.push(part);
|
||
}
|
||
});
|
||
if (andParts.length > 0) {
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
stmt2 += `(${andParts.join(' AND ')})`;
|
||
}
|
||
}
|
||
else if (attr2 === '$or') {
|
||
const orParts = [];
|
||
o[attr2].forEach((ele) => {
|
||
const part = translateInner(ele, p);
|
||
if (part) {
|
||
orParts.push(part);
|
||
}
|
||
});
|
||
if (orParts.length > 0) {
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
stmt2 += `(${orParts.join(' OR ')})`;
|
||
}
|
||
}
|
||
else if (attr2 === '$contains') {
|
||
// PostgreSQL 使用 @> 操作符检查 JSON 包含关系
|
||
const targetArr = Array.isArray(o[attr2]) ? o[attr2] : [o[attr2]];
|
||
const value = JSON.stringify(targetArr);
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
if (p) {
|
||
// 先提取子路径,再检查包含
|
||
const accessor = this.buildJsonAccessor(columnRef, p, false);
|
||
stmt2 += `(${accessor} @> '${value}'::jsonb)`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} @> '${value}'::jsonb)`;
|
||
}
|
||
}
|
||
else if (attr2 === '$overlaps') {
|
||
// PostgreSQL 使用 ?| 检查数组键重叠,或使用自定义逻辑
|
||
const value = o[attr2];
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
if (Array.isArray(value)) {
|
||
// 如果是数组,检查是否有任意元素匹配
|
||
const accessor = p ? this.buildJsonAccessor(columnRef, p, false) : columnRef;
|
||
// 使用 @> 操作符检查数组是否包含任意一个值(支持数字和字符串)
|
||
const conditions = value.map(v => `${accessor} @> '${JSON.stringify(v)}'::jsonb`);
|
||
stmt2 += `(${conditions.join(' OR ')})`;
|
||
}
|
||
else if (typeof value === 'object' && value !== null) {
|
||
// 对象重叠检查 - 检查是否有共同的键
|
||
const keys = Object.keys(value);
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, false);
|
||
stmt2 += `(${accessor} ?| array[${keys.map(k => `'${k}'`).join(', ')}])`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} ?| array[${keys.map(k => `'${k}'`).join(', ')}])`;
|
||
}
|
||
}
|
||
else {
|
||
(0, assert_1.default)(typeof value === 'string');
|
||
// 单个键的重叠检查
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, false);
|
||
stmt2 += `(${accessor} ? '${value}')`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} ? '${value}')`;
|
||
}
|
||
}
|
||
}
|
||
else if (attr2 === '$length') {
|
||
// PostgreSQL 使用 jsonb_array_length
|
||
const length = o[attr2];
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
let lengthExpr;
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, false);
|
||
lengthExpr = `jsonb_array_length(${accessor})`;
|
||
}
|
||
else {
|
||
lengthExpr = `jsonb_array_length(${columnRef})`;
|
||
}
|
||
if (typeof length === 'number') {
|
||
stmt2 += `(${lengthExpr} = ${length})`;
|
||
}
|
||
else {
|
||
(0, assert_1.default)(typeof length === 'object');
|
||
const op = Object.keys(length)[0];
|
||
(0, assert_1.default)(op.startsWith('$'));
|
||
stmt2 += `(${lengthExpr} ${this.translatePredicate(op, length[op])})`;
|
||
}
|
||
}
|
||
else if (attr2 === '$exists') {
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
const existsValue = o[attr2];
|
||
if (typeof existsValue === 'boolean') {
|
||
// $exists: true/false - 检查当前路径的值是否存在(不为 null)
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, true);
|
||
if (existsValue) {
|
||
stmt2 += `(${accessor} IS NOT NULL)`;
|
||
}
|
||
else {
|
||
stmt2 += `(${accessor} IS NULL)`;
|
||
}
|
||
}
|
||
else {
|
||
if (existsValue) {
|
||
stmt2 += `(${columnRef} IS NOT NULL)`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} IS NULL)`;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// $exists: 'keyName' - 检查 JSON 对象是否包含指定的键
|
||
const keyToCheck = existsValue;
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, false);
|
||
stmt2 += `(${accessor} ? '${keyToCheck}')`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} ? '${keyToCheck}')`;
|
||
}
|
||
}
|
||
}
|
||
else if (attr2.startsWith('$')) {
|
||
// 其他操作符:$gt, $lt, $eq 等
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, true);
|
||
// JSON 提取的文本需要转换类型进行比较
|
||
const predicate = this.translatePredicate(attr2, o[attr2]);
|
||
if (typeof o[attr2] === 'number') {
|
||
stmt2 += `((${accessor})::numeric ${predicate})`;
|
||
}
|
||
else if (typeof o[attr2] === 'boolean') {
|
||
stmt2 += `((${accessor})::boolean ${predicate})`;
|
||
}
|
||
else {
|
||
stmt2 += `(${accessor} ${predicate})`;
|
||
}
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} ${this.translatePredicate(attr2, o[attr2])})`;
|
||
}
|
||
}
|
||
else {
|
||
// 继续子对象解构
|
||
const attr3 = attr2.startsWith('.') ? attr2.slice(1) : attr2;
|
||
const part = translateInner(o[attr2], p ? `${p}.${attr3}` : `.${attr3}`);
|
||
if (part) {
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
stmt2 += part;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// 直接的值比较
|
||
if (stmt2) {
|
||
stmt2 += ' AND ';
|
||
}
|
||
if (p) {
|
||
const accessor = this.buildJsonAccessor(columnRef, p, true);
|
||
if (typeof o === 'string') {
|
||
stmt2 += `(${accessor} = '${o}')`;
|
||
}
|
||
else if (typeof o === 'number') {
|
||
stmt2 += `((${accessor})::numeric = ${o})`;
|
||
}
|
||
else if (typeof o === 'boolean') {
|
||
stmt2 += `((${accessor})::boolean = ${o})`;
|
||
}
|
||
else {
|
||
stmt2 += `(${accessor} = ${o})`;
|
||
}
|
||
}
|
||
else {
|
||
// 对根对象的比较
|
||
if (typeof o === 'string') {
|
||
stmt2 += `(${columnRef}::text = '${o}')`;
|
||
}
|
||
else {
|
||
stmt2 += `(${columnRef} = '${JSON.stringify(o)}'::jsonb)`;
|
||
}
|
||
}
|
||
}
|
||
return stmt2;
|
||
};
|
||
return translateInner(predicate, '');
|
||
}
|
||
translateObjectProjection(projection, alias, attr, prefix) {
|
||
const columnRef = `"${alias}"."${attr}"`;
|
||
let stmt = '';
|
||
const translateInner = (o, p) => {
|
||
if (o instanceof Array) {
|
||
o.forEach((item, idx) => {
|
||
const p2 = `${p}[${idx}]`;
|
||
if (typeof item === 'number') {
|
||
// 数字表示选择该位置的值
|
||
if (stmt) {
|
||
stmt += ', ';
|
||
}
|
||
const accessor = this.buildJsonAccessor(columnRef, p2, true);
|
||
stmt += accessor;
|
||
// 生成别名,将路径中的特殊字符替换
|
||
stmt += prefix ? ` AS "${prefix}.${attr}${p2}"` : ` AS "${attr}${p2}"`;
|
||
}
|
||
else if (typeof item === 'object') {
|
||
translateInner(item, p2);
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
for (const key in o) {
|
||
const p2 = p ? `${p}.${key}` : `.${key}`;
|
||
if (typeof o[key] === 'number') {
|
||
// 数字表示选择该字段
|
||
if (stmt) {
|
||
stmt += ', ';
|
||
}
|
||
const accessor = this.buildJsonAccessor(columnRef, p2, true);
|
||
stmt += accessor;
|
||
// 生成别名
|
||
stmt += prefix ? ` AS "${prefix}.${attr}${p2}"` : ` AS "${attr}${p2}"`;
|
||
}
|
||
else {
|
||
translateInner(o[key], p2);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
translateInner(projection, '');
|
||
return stmt;
|
||
}
|
||
translateAttrValue(dataType, value) {
|
||
if (value === null || value === undefined) {
|
||
return 'null';
|
||
}
|
||
switch (dataType) {
|
||
case 'geometry': {
|
||
return transformGeoData(value);
|
||
}
|
||
case 'datetime':
|
||
case 'time':
|
||
case 'date': {
|
||
if (value instanceof Date) {
|
||
return `${value.valueOf()}`;
|
||
}
|
||
else if (typeof value === 'number') {
|
||
return `${value}`;
|
||
}
|
||
(0, assert_1.default)(typeof value === 'string', 'Invalid date/time value');
|
||
return `'${(new Date(value)).valueOf()}'`;
|
||
}
|
||
case 'object':
|
||
case 'array': {
|
||
// PostgreSQL JSONB 需要显式类型转换
|
||
const jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
||
return `'${jsonStr}'::jsonb`;
|
||
}
|
||
/* case 'function': {
|
||
return `'${Buffer.from(value.toString()).toString('base64')}'`;
|
||
} */
|
||
default: {
|
||
if (typeof value === 'string') {
|
||
return this.escapeStringValue(value);
|
||
}
|
||
return value;
|
||
}
|
||
}
|
||
}
|
||
translateFullTextSearch(value, entity, alias) {
|
||
const { $search, $ts } = value;
|
||
const { indexes } = this.schema[entity];
|
||
const ftIndex = indexes && indexes.find((ele) => {
|
||
const { config } = ele;
|
||
return config && config.type === 'fulltext';
|
||
});
|
||
(0, assert_1.default)(ftIndex, `Entity ${String(entity)} does not have a fulltext index`);
|
||
const { attributes } = ftIndex;
|
||
// PostgreSQL 全文搜索使用 to_tsvector 和 to_tsquery
|
||
// 将多个列合并成一个文档
|
||
const columns = attributes.map(({ name }) => `COALESCE("${alias}"."${name}", '')`).join(" || ' ' || ");
|
||
// 处理搜索词:将空格分隔的词转换为 & 连接
|
||
const searchTerms = $search
|
||
.trim()
|
||
.split(/\s+/)
|
||
.filter(term => term.length > 0)
|
||
.map(term => this.escapeTsQueryValue(term))
|
||
.filter(term => term.length > 0) // 过滤掉转义后为空的词
|
||
.join(' & ');
|
||
if (!searchTerms) {
|
||
// 如果没有有效的搜索词,返回一个始终为假的条件,以避免匹配任何记录
|
||
console.error('Full-text search: no valid search terms after escaping, returning FALSE condition');
|
||
return 'FALSE';
|
||
}
|
||
const indexTsConfig = ftIndex.config?.tsConfig;
|
||
let searchTsConfig;
|
||
if (Array.isArray(indexTsConfig)) {
|
||
// 如果是Array的情况,则需要去判断一下在不在索引定义中,没定义的话要警告
|
||
if (!$ts) {
|
||
if (indexTsConfig.length === 0) {
|
||
// 没有$ts也没配置索引的情况,和定义的时候一样默认走simple
|
||
searchTsConfig = 'simple';
|
||
}
|
||
else {
|
||
// 如果没提供$ts,则无法确定唯一的索引方法,目前先走索引定义时的第一个
|
||
console.warn(`Full-text search: Entity: ${String(entity)} has multi fulltext Index: ${indexTsConfig.join(', ')} please provide a specific "$ts" value! now using: ${indexTsConfig[0]}`);
|
||
return `to_tsvector('${indexTsConfig[0]}', ${columns}) @@ to_tsquery('simple', '${searchTerms}')`; // 使用simple去查询,这里就可能不走索引
|
||
}
|
||
}
|
||
else {
|
||
// 提供了ts,找indexTsConfig中符合$ts的存在
|
||
const find = indexTsConfig.findIndex(item => item === $ts);
|
||
if (find === -1) {
|
||
// 如果$ts 在indexTsConfig里面没定义,说明大概率查询的时候不会走索引,需要给个警告
|
||
console.warn(`Full-text search: provided "$ts" didnt match any tsConfig in the fulltext Index: ${ftIndex.name} on Entity: ${String(entity)}`);
|
||
}
|
||
// 提供了就用
|
||
searchTsConfig = $ts;
|
||
}
|
||
}
|
||
else {
|
||
searchTsConfig = $ts || indexTsConfig || 'simple'; // 这里的使用顺序是,$ts > indexTsConfig > default('simple')
|
||
}
|
||
return `to_tsvector('${searchTsConfig}', ${columns}) @@ to_tsquery('${searchTsConfig}', '${searchTerms}')`;
|
||
}
|
||
translateCreateEntity(entity, options) {
|
||
const ifExists = options?.ifExists || 'drop';
|
||
const { schema } = this;
|
||
const entityDef = schema[entity];
|
||
const { storageName, attributes, indexes, view, static: _static } = entityDef;
|
||
const tableName = storageName || entity;
|
||
const sqls = [];
|
||
// 收集需要创建的 enum 类型
|
||
const enumTypesToCreate = [];
|
||
// 处理删除逻辑
|
||
if (ifExists === 'drop' || (!_static && ifExists === 'dropIfNotStatic')) {
|
||
// 先删除表
|
||
sqls.push(`DROP TABLE IF EXISTS "${tableName}" CASCADE;`);
|
||
}
|
||
// 预处理:检查所有 enum 类型
|
||
for (const attr in attributes) {
|
||
const attrDef = attributes[attr];
|
||
if (attrDef.type === 'enum' && attrDef.enumeration) {
|
||
const enumTypeName = this.getEnumTypeName(tableName, attr);
|
||
enumTypesToCreate.push({
|
||
name: enumTypeName,
|
||
values: attrDef.enumeration
|
||
});
|
||
}
|
||
}
|
||
// 删除并重建 enum 类型
|
||
for (const enumType of enumTypesToCreate) {
|
||
if (ifExists === 'drop' || (!_static && ifExists === 'dropIfNotStatic')) {
|
||
sqls.push(`DROP TYPE IF EXISTS "${enumType.name}" CASCADE;`);
|
||
}
|
||
const createEnumSql = `CREATE TYPE "${enumType.name}" AS ENUM (${enumType.values.map(v => `'${v}'`).join(', ')});`;
|
||
if (ifExists === 'omit' || (_static && ifExists === 'dropIfNotStatic')) {
|
||
sqls.push(`DO $$ BEGIN ${createEnumSql} EXCEPTION WHEN duplicate_object THEN NULL; END $$;`);
|
||
}
|
||
else {
|
||
sqls.push(createEnumSql);
|
||
}
|
||
}
|
||
// todo view 暂还不支持
|
||
if (view) {
|
||
throw new Error('view unsupported yet');
|
||
}
|
||
// 构建 CREATE TABLE 语句
|
||
let createTableSql = 'CREATE TABLE ';
|
||
if (ifExists === 'omit' || (_static && ifExists === 'dropIfNotStatic')) {
|
||
createTableSql += 'IF NOT EXISTS ';
|
||
}
|
||
createTableSql += `"${tableName}" (\n`;
|
||
// 翻译所有的属性
|
||
const columnDefs = [];
|
||
let hasSequence = null;
|
||
for (const attr in attributes) {
|
||
const attrDef = attributes[attr];
|
||
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
||
let columnDef = ` "${attr}" `;
|
||
// 处理 enum 类型
|
||
if (type === 'enum') {
|
||
const enumTypeName = this.getEnumTypeName(tableName, attr);
|
||
columnDef += `"${enumTypeName}"`;
|
||
}
|
||
else {
|
||
columnDef += this.populateDataTypeDef(type, params, enumeration);
|
||
}
|
||
// NOT NULL 约束
|
||
if (notNull || type === 'geometry') {
|
||
columnDef += ' NOT NULL';
|
||
}
|
||
// UNIQUE 约束
|
||
if (unique) {
|
||
columnDef += ' UNIQUE';
|
||
}
|
||
// 序列处理 - PostgreSQL 使用 GENERATED AS IDENTITY
|
||
if (sequenceStart) {
|
||
if (hasSequence) {
|
||
throw new Error(`「${entity}」只能有一个 sequence 列`);
|
||
}
|
||
hasSequence = attr;
|
||
// 替换类型为支持自增的类型
|
||
columnDef = ` "${attr}" bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart}) UNIQUE`;
|
||
}
|
||
// 默认值
|
||
if (defaultValue !== undefined && !sequenceStart) {
|
||
(0, assert_1.default)(type !== 'ref');
|
||
columnDef += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
|
||
}
|
||
// 主键
|
||
if (attr === 'id') {
|
||
columnDef += ' PRIMARY KEY';
|
||
}
|
||
columnDefs.push(columnDef);
|
||
}
|
||
createTableSql += columnDefs.join(',\n');
|
||
createTableSql += '\n);';
|
||
sqls.push(createTableSql);
|
||
// 单独创建索引 - PostgreSQL 索引必须在表外定义
|
||
if (indexes) {
|
||
for (const indexDef of indexes) {
|
||
const { name, attributes: indexAttrs, config } = indexDef;
|
||
const { unique, type: indexType, tsConfig } = config || {};
|
||
// 确定需要创建的索引配置列表
|
||
const indexConfigs = [];
|
||
if (indexType === 'fulltext') {
|
||
// 全文索引:根据 tsConfig 确定要创建的索引
|
||
if (Array.isArray(tsConfig)) {
|
||
if (tsConfig.length === 0) {
|
||
console.warn(`entity: ${String(entity)} has empty tsConfig array on fulltext index: ${name}, using 'simple' for default.`);
|
||
indexConfigs.push({ suffix: '', tsLang: 'simple' });
|
||
}
|
||
else if (tsConfig.length === 1) {
|
||
indexConfigs.push({ suffix: '', tsLang: tsConfig[0] });
|
||
}
|
||
else {
|
||
// 多语言:为每个语言创建独立索引
|
||
for (const lang of tsConfig) {
|
||
indexConfigs.push({ suffix: `_${lang}`, tsLang: lang });
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
indexConfigs.push({ suffix: '', tsLang: tsConfig || 'simple' });
|
||
}
|
||
}
|
||
else {
|
||
// 非全文索引:只创建一个索引
|
||
indexConfigs.push({ suffix: '' });
|
||
}
|
||
// 为每个配置创建索引
|
||
for (const indexConfig of indexConfigs) {
|
||
const { suffix, tsLang } = indexConfig;
|
||
let indexSql = 'CREATE ';
|
||
if (unique) {
|
||
indexSql += 'UNIQUE ';
|
||
}
|
||
indexSql += 'INDEX ';
|
||
if (ifExists === 'omit' || (_static && ifExists === 'dropIfNotStatic')) {
|
||
indexSql += 'IF NOT EXISTS ';
|
||
}
|
||
// 索引名称(多语言时添加后缀)
|
||
indexSql += `"${String(entity)}_${name}${suffix}" ON "${tableName}" `;
|
||
// 索引方法
|
||
if (indexType === 'hash') {
|
||
indexSql += 'USING HASH ';
|
||
}
|
||
else if (indexType === 'spatial') {
|
||
indexSql += 'USING GIST ';
|
||
}
|
||
else if (indexType === 'fulltext') {
|
||
indexSql += 'USING GIN ';
|
||
}
|
||
indexSql += '(';
|
||
const indexColumns = [];
|
||
for (const indexAttr of indexAttrs) {
|
||
const { name: attrName, direction } = indexAttr;
|
||
if (indexType === 'fulltext') {
|
||
// 全文索引:使用 to_tsvector
|
||
indexColumns.push(`to_tsvector('${tsLang}', COALESCE("${attrName}", ''))`);
|
||
}
|
||
else {
|
||
// 普通索引:直接使用列名
|
||
let col = `"${attrName}"`;
|
||
if (direction) {
|
||
col += ` ${direction}`;
|
||
}
|
||
indexColumns.push(col);
|
||
}
|
||
}
|
||
indexSql += indexColumns.join(', ');
|
||
indexSql += ');';
|
||
sqls.push(indexSql);
|
||
}
|
||
}
|
||
}
|
||
return sqls;
|
||
}
|
||
translateFnName(fnName, argumentNumber) {
|
||
switch (fnName) {
|
||
// ========== 数学运算 ==========
|
||
case '$add': {
|
||
let result = '%s';
|
||
while (--argumentNumber > 0) {
|
||
result += ' + %s';
|
||
}
|
||
return result;
|
||
}
|
||
case '$subtract': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s - %s';
|
||
}
|
||
case '$multiply': {
|
||
let result = '%s';
|
||
while (--argumentNumber > 0) {
|
||
result += ' * %s';
|
||
}
|
||
return result;
|
||
}
|
||
case '$divide': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s / %s';
|
||
}
|
||
case '$abs': {
|
||
return 'ABS(%s)';
|
||
}
|
||
case '$round': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return 'ROUND(%s, %s)';
|
||
}
|
||
case '$ceil': {
|
||
return 'CEIL(%s)';
|
||
}
|
||
case '$floor': {
|
||
return 'FLOOR(%s)';
|
||
}
|
||
case '$mod': {
|
||
// PostgreSQL 使用 MOD 函数或 % 操作符
|
||
return 'MOD(%s, %s)';
|
||
}
|
||
case '$pow': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
// PostgreSQL 使用 POWER
|
||
return 'POWER(%s, %s)';
|
||
}
|
||
// ========== 比较运算 ==========
|
||
case '$gt': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s > %s';
|
||
}
|
||
case '$gte': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s >= %s';
|
||
}
|
||
case '$lt': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s < %s';
|
||
}
|
||
case '$lte': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s <= %s';
|
||
}
|
||
case '$eq': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s = %s';
|
||
}
|
||
case '$ne': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s <> %s';
|
||
}
|
||
// ========== 字符串操作 ==========
|
||
case '$startsWith': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
// PostgreSQL 使用 || 进行字符串连接
|
||
return '%s LIKE %s || \'%%\'';
|
||
}
|
||
case '$endsWith': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s LIKE \'%%\' || %s';
|
||
}
|
||
case '$includes': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '%s LIKE \'%%\' || %s || \'%%\'';
|
||
}
|
||
case '$concat': {
|
||
// PostgreSQL 支持 CONCAT 函数
|
||
let result = 'CONCAT(%s';
|
||
while (--argumentNumber > 0) {
|
||
result += ', %s';
|
||
}
|
||
result += ')';
|
||
return result;
|
||
}
|
||
// ========== 布尔运算 ==========
|
||
case '$true': {
|
||
return '( %s ) = TRUE';
|
||
}
|
||
case '$false': {
|
||
return '( %s ) = FALSE';
|
||
}
|
||
case '$and': {
|
||
let result = '';
|
||
for (let iter = 0; iter < argumentNumber; iter++) {
|
||
result += '%s';
|
||
if (iter < argumentNumber - 1) {
|
||
result += ' AND ';
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
case '$or': {
|
||
let result = '';
|
||
for (let iter = 0; iter < argumentNumber; iter++) {
|
||
result += '%s';
|
||
if (iter < argumentNumber - 1) {
|
||
result += ' OR ';
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
case '$not': {
|
||
return 'NOT %s';
|
||
}
|
||
// ========== 日期时间函数 ==========
|
||
case '$year': {
|
||
return 'EXTRACT(YEAR FROM %s)::integer';
|
||
}
|
||
case '$month': {
|
||
return 'EXTRACT(MONTH FROM %s)::integer';
|
||
}
|
||
case '$day': {
|
||
return 'EXTRACT(DAY FROM %s)::integer';
|
||
}
|
||
case '$hour': {
|
||
return 'EXTRACT(HOUR FROM %s)::integer';
|
||
}
|
||
case '$minute': {
|
||
return 'EXTRACT(MINUTE FROM %s)::integer';
|
||
}
|
||
case '$second': {
|
||
return 'EXTRACT(SECOND FROM %s)::integer';
|
||
}
|
||
case '$weekday': {
|
||
// PostgreSQL ISODOW: 1(周一) - 7(周日)
|
||
// 转换为 0(周一) - 6(周日) 以兼容 MySQL
|
||
return '(EXTRACT(ISODOW FROM %s)::integer - 1)';
|
||
}
|
||
case '$weekOfYear': {
|
||
return 'EXTRACT(WEEK FROM %s)::integer';
|
||
}
|
||
case '$dayOfMonth': {
|
||
return 'EXTRACT(DAY FROM %s)::integer';
|
||
}
|
||
case '$dayOfWeek': {
|
||
// PostgreSQL DOW: 0(周日) - 6(周六)
|
||
// MySQL DAYOFWEEK: 1(周日) - 7(周六)
|
||
// 转换以兼容 MySQL
|
||
return '(EXTRACT(DOW FROM %s)::integer + 1)';
|
||
}
|
||
case '$dayOfYear': {
|
||
return 'EXTRACT(DOY FROM %s)::integer';
|
||
}
|
||
// 下面两个函数需要特殊处理,放在 translateExpression 里实现
|
||
// case '$dateDiff': {
|
||
// // PostgreSQL 日期差值计算
|
||
// // 参数: unit, date1, date2
|
||
// // 返回 date1 - date2 的差值
|
||
// assert(argumentNumber === 3);
|
||
// // 注意:这里需要特殊处理,因为 PostgreSQL 的日期差异计算方式不同
|
||
// return 'EXTRACT(EPOCH FROM (%s - %s))::integer / 86400';
|
||
// }
|
||
// case '$dateAdd': {
|
||
// // 日期加法
|
||
// assert(argumentNumber === 3);
|
||
// // 参数: date, amount, unit
|
||
// return '(%s + INTERVAL \'1 %s\' * %s)';
|
||
// }
|
||
case '$dateCeil': {
|
||
// 日期向上取整 - 参数: [date, unit]
|
||
// 在 translateExpression 中特殊处理
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '__DATE_CEIL__(%s, %s)';
|
||
}
|
||
case '$dateFloor': {
|
||
// 日期向下取整 - 参数: [date, unit]
|
||
// 在 translateExpression 中特殊处理
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return '__DATE_FLOOR__(%s, %s)';
|
||
}
|
||
// ========== 地理空间函数 (PostGIS) ==========
|
||
case '$contains': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return 'ST_Contains(%s, %s)';
|
||
}
|
||
case '$distance': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return 'ST_Distance(%s, %s)';
|
||
}
|
||
case '$within': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return 'ST_Within(%s, %s)';
|
||
}
|
||
case '$intersects': {
|
||
(0, assert_1.default)(argumentNumber === 2);
|
||
return 'ST_Intersects(%s, %s)';
|
||
}
|
||
// ========== 聚合函数 ==========
|
||
case '$$count': {
|
||
return 'COUNT(%s)';
|
||
}
|
||
case '$$sum': {
|
||
return 'SUM(%s)';
|
||
}
|
||
case '$$max': {
|
||
return 'MAX(%s)';
|
||
}
|
||
case '$$min': {
|
||
return 'MIN(%s)';
|
||
}
|
||
case '$$avg': {
|
||
return 'AVG(%s)';
|
||
}
|
||
default: {
|
||
throw new Error(`unrecognized function ${fnName}`);
|
||
}
|
||
}
|
||
}
|
||
translateAttrInExpression(entity, attr, exprText) {
|
||
const { attributes } = this.schema[entity];
|
||
const attrDef = attributes[attr];
|
||
if (!attrDef) {
|
||
return exprText;
|
||
}
|
||
const { type } = attrDef;
|
||
if (['date', 'time', 'datetime'].includes(type)) {
|
||
// 从 Unix 时间戳(毫秒)转成 timestamp 类型参加 expr 的运算
|
||
// PostgreSQL 使用 TO_TIMESTAMP,参数为秒
|
||
return `TO_TIMESTAMP(${exprText}::double precision / 1000)`;
|
||
}
|
||
return exprText;
|
||
}
|
||
translateExpression(entity, alias, expression, refDict) {
|
||
const translateConstant = (constant) => {
|
||
if (constant instanceof Date) {
|
||
// PostgreSQL 使用 TO_TIMESTAMP,参数为秒
|
||
return `TO_TIMESTAMP(${constant.valueOf()}::double precision / 1000)`;
|
||
}
|
||
else if (typeof constant === 'string') {
|
||
// 转义单引号
|
||
return `'${constant.replace(/'/g, "''")}'`;
|
||
}
|
||
else {
|
||
(0, assert_1.default)(typeof constant === 'number');
|
||
return `${constant}`;
|
||
}
|
||
};
|
||
const translateInner = (expr) => {
|
||
const k = Object.keys(expr);
|
||
let result;
|
||
if (k.includes('#attr')) {
|
||
const attrName = (expr)['#attr'];
|
||
const attrText = `"${alias}"."${attrName}"`;
|
||
result = this.translateAttrInExpression(entity, attrName, attrText);
|
||
}
|
||
else if (k.includes('#refId')) {
|
||
const refId = (expr)['#refId'];
|
||
const refAttr = (expr)['#refAttr'];
|
||
(0, assert_1.default)(refDict[refId]);
|
||
const [refAlias, refEntity] = refDict[refId];
|
||
const attrText = `"${refAlias}"."${refAttr}"`;
|
||
result = this.translateAttrInExpression(refEntity, refAttr, attrText);
|
||
}
|
||
else {
|
||
(0, assert_1.default)(k.length === 1);
|
||
const fnKey = k[0];
|
||
const fnArgs = (expr)[fnKey];
|
||
// 特殊处理日期相关函数
|
||
if (fnKey === '$dateDiff') {
|
||
// $dateDiff: [date1, date2, unit]
|
||
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 3);
|
||
const [date1Expr, date2Expr, unit] = fnArgs;
|
||
// 转换日期表达式
|
||
const translateDateArg = (arg) => {
|
||
if (arg instanceof Date) {
|
||
return `TO_TIMESTAMP(${arg.valueOf()}::double precision / 1000)`;
|
||
}
|
||
else if (typeof arg === 'number') {
|
||
return `TO_TIMESTAMP(${arg}::double precision / 1000)`;
|
||
}
|
||
else {
|
||
return translateInner(arg);
|
||
}
|
||
};
|
||
const date1Str = translateDateArg(date1Expr);
|
||
const date2Str = translateDateArg(date2Expr);
|
||
// 根据单位生成不同的 SQL
|
||
switch (unit) {
|
||
case 's':
|
||
result = `EXTRACT(EPOCH FROM (${date1Str} - ${date2Str}))::bigint`;
|
||
break;
|
||
case 'm':
|
||
result = `(EXTRACT(EPOCH FROM (${date1Str} - ${date2Str})) / 60)::bigint`;
|
||
break;
|
||
case 'h':
|
||
result = `(EXTRACT(EPOCH FROM (${date1Str} - ${date2Str})) / 3600)::bigint`;
|
||
break;
|
||
case 'd':
|
||
result = `(EXTRACT(EPOCH FROM (${date1Str} - ${date2Str})) / 86400)::bigint`;
|
||
break;
|
||
case 'M':
|
||
// 月份差异使用 AGE 函数
|
||
result = `(EXTRACT(YEAR FROM AGE(${date1Str}, ${date2Str})) * 12 + EXTRACT(MONTH FROM AGE(${date1Str}, ${date2Str})))::bigint`;
|
||
break;
|
||
case 'y':
|
||
result = `EXTRACT(YEAR FROM AGE(${date1Str}, ${date2Str}))::bigint`;
|
||
break;
|
||
default:
|
||
throw new Error(`Unsupported date diff unit: ${unit}`);
|
||
}
|
||
}
|
||
else if (fnKey === '$dateCeil') {
|
||
// $dateCeil: [date, unit]
|
||
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 2);
|
||
const [dateExpr, unit] = fnArgs;
|
||
// 获取毫秒时间戳表达式
|
||
const getTimestampExpr = (arg) => {
|
||
if (arg instanceof Date) {
|
||
return `${arg.valueOf()}`;
|
||
}
|
||
else if (typeof arg === 'number') {
|
||
return `${arg}`;
|
||
}
|
||
else {
|
||
// 属性引用,直接返回属性(存储本身就是毫秒时间戳)
|
||
const k = Object.keys(arg);
|
||
if (k.includes('#attr')) {
|
||
return `"${alias}"."${arg['#attr']}"`;
|
||
}
|
||
else if (k.includes('#refId')) {
|
||
const refId = arg['#refId'];
|
||
const refAttr = arg['#refAttr'];
|
||
return `"${refDict[refId][0]}"."${refAttr}"`;
|
||
}
|
||
// 其他表达式递归处理
|
||
return translateInner(arg);
|
||
}
|
||
};
|
||
const tsExpr = getTimestampExpr(dateExpr);
|
||
// 固定间隔单位:直接用时间戳数学运算
|
||
const msPerUnit = {
|
||
's': 1000,
|
||
'm': 60000,
|
||
'h': 3600000,
|
||
'd': 86400000,
|
||
};
|
||
if (msPerUnit[unit]) {
|
||
// CEIL 向上取整:CEIL(timestamp / interval) * interval
|
||
result = `CEIL(${tsExpr}::numeric / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||
}
|
||
else {
|
||
// 月和年需要用日期函数,但最终转回时间戳
|
||
const dateStr = `TO_TIMESTAMP(${tsExpr}::double precision / 1000)`;
|
||
const unitMap = {
|
||
'M': 'month',
|
||
'y': 'year'
|
||
};
|
||
const pgUnit = unitMap[unit];
|
||
if (!pgUnit) {
|
||
throw new Error(`Unsupported date ceil unit: ${unit}`);
|
||
}
|
||
// 向上取整:如果截断后不等于原值,则加一个单位,最后转回毫秒时间戳
|
||
result = `(EXTRACT(EPOCH FROM (CASE WHEN DATE_TRUNC('${pgUnit}', ${dateStr}) = ${dateStr} THEN ${dateStr} ELSE DATE_TRUNC('${pgUnit}', ${dateStr}) + INTERVAL '1 ${pgUnit}' END)) * 1000)::bigint`;
|
||
}
|
||
}
|
||
else if (fnKey === '$dateFloor') {
|
||
// $dateFloor: [date, unit]
|
||
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 2);
|
||
const [dateExpr, unit] = fnArgs;
|
||
// 获取毫秒时间戳表达式
|
||
const getTimestampExpr = (arg) => {
|
||
if (arg instanceof Date) {
|
||
return `${arg.valueOf()}`;
|
||
}
|
||
else if (typeof arg === 'number') {
|
||
return `${arg}`;
|
||
}
|
||
else {
|
||
// 属性引用,直接返回属性(存储本身就是毫秒时间戳)
|
||
const k = Object.keys(arg);
|
||
if (k.includes('#attr')) {
|
||
return `"${alias}"."${arg['#attr']}"`;
|
||
}
|
||
else if (k.includes('#refId')) {
|
||
const refId = arg['#refId'];
|
||
const refAttr = arg['#refAttr'];
|
||
return `"${refDict[refId][0]}"."${refAttr}"`;
|
||
}
|
||
// 其他表达式递归处理
|
||
return translateInner(arg);
|
||
}
|
||
};
|
||
const tsExpr = getTimestampExpr(dateExpr);
|
||
// 固定间隔单位:直接用时间戳数学运算
|
||
const msPerUnit = {
|
||
's': 1000,
|
||
'm': 60000,
|
||
'h': 3600000,
|
||
'd': 86400000,
|
||
};
|
||
if (msPerUnit[unit]) {
|
||
// FLOOR 向下取整:FLOOR(timestamp / interval) * interval
|
||
result = `FLOOR(${tsExpr}::numeric / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||
}
|
||
else {
|
||
// 月和年需要用日期函数,但最终转回时间戳
|
||
const dateStr = `TO_TIMESTAMP(${tsExpr}::double precision / 1000)`;
|
||
const unitMap = {
|
||
'M': 'month',
|
||
'y': 'year'
|
||
};
|
||
const pgUnit = unitMap[unit];
|
||
if (!pgUnit) {
|
||
throw new Error(`Unsupported date floor unit: ${unit}`);
|
||
}
|
||
// DATE_TRUNC 向下取整,转回毫秒时间戳
|
||
result = `(EXTRACT(EPOCH FROM DATE_TRUNC('${pgUnit}', ${dateStr})) * 1000)::bigint`;
|
||
}
|
||
}
|
||
else if (fnArgs instanceof Array) {
|
||
// 原有的数组参数处理逻辑
|
||
const fnName = this.translateFnName(fnKey, fnArgs.length);
|
||
const args = [fnName];
|
||
args.push(...fnArgs.map((ele) => {
|
||
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
|
||
return translateConstant(ele);
|
||
}
|
||
else {
|
||
return translateInner(ele);
|
||
}
|
||
}));
|
||
result = util_1.format.apply(null, args);
|
||
}
|
||
else {
|
||
// 原有的单参数处理逻辑
|
||
const fnName = this.translateFnName(fnKey, 1);
|
||
const args = [fnName];
|
||
if (['string', 'number'].includes(typeof fnArgs) || fnArgs instanceof Date) {
|
||
args.push(translateConstant(fnArgs));
|
||
}
|
||
else {
|
||
args.push(translateInner(fnArgs));
|
||
}
|
||
result = util_1.format.apply(null, args);
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
return translateInner(expression);
|
||
}
|
||
populateSelectStmt(projectionText, fromText, aliasDict, filterText, sorterText, groupByText, indexFrom, count, option) {
|
||
let sql = `SELECT ${projectionText} FROM ${fromText}`;
|
||
if (filterText) {
|
||
sql += ` WHERE ${filterText}`;
|
||
}
|
||
if (groupByText) {
|
||
sql += ` GROUP BY ${groupByText}`;
|
||
}
|
||
if (sorterText) {
|
||
sql += ` ORDER BY ${sorterText}`;
|
||
}
|
||
// PostgreSQL 语法: LIMIT count OFFSET offset
|
||
if (typeof count === 'number') {
|
||
sql += ` LIMIT ${count}`;
|
||
}
|
||
if (typeof indexFrom === 'number' && indexFrom > 0) {
|
||
sql += ` OFFSET ${indexFrom}`;
|
||
}
|
||
// FOR UPDATE 锁定
|
||
if (option?.forUpdate) {
|
||
sql += ' FOR UPDATE';
|
||
if (typeof option.forUpdate === 'string') {
|
||
// PostgreSQL 支持: NOWAIT, SKIP LOCKED, OF table_name
|
||
sql += ` ${option.forUpdate}`;
|
||
}
|
||
}
|
||
return sql;
|
||
}
|
||
translateUpdate(entity, operation, option) {
|
||
const { attributes } = this.schema[entity];
|
||
const { filter, sorter, indexFrom, count, data } = operation;
|
||
(0, assert_1.default)(!sorter, '当前update不支持sorter行为');
|
||
// 使用结构化的JOIN分析
|
||
const { aliasDict, filterRefAlias, mainTable, mainAlias, joinInfos, currentNumber } = this.analyzeJoinStructured(entity, { filter, sorter });
|
||
// 构建SET子句 - PostgreSQL中SET子句不能使用表别名前缀
|
||
const setClauses = [];
|
||
for (const attr in data) {
|
||
(0, assert_1.default)(attributes.hasOwnProperty(attr));
|
||
const value = this.translateAttrValue(attributes[attr].type, data[attr]);
|
||
setClauses.push(`"${attr}" = ${value}`);
|
||
}
|
||
const setClause = setClauses.join(', ');
|
||
// 构建过滤条件
|
||
const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, option);
|
||
let sql;
|
||
if (joinInfos.length === 0) {
|
||
// 单表更新
|
||
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
||
if (filterText) {
|
||
sql += ` WHERE ${filterText}`;
|
||
}
|
||
}
|
||
else {
|
||
// 多表更新 - PostgreSQL语法: UPDATE main SET ... FROM other_tables WHERE join_conditions AND filter
|
||
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
||
// FROM子句包含所有JOIN的表
|
||
const fromTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
|
||
sql += ` FROM ${fromTables}`;
|
||
// WHERE子句包含JOIN条件和过滤条件
|
||
const joinConditions = this.buildJoinConditions(joinInfos);
|
||
let whereClause = joinConditions;
|
||
if (filterText) {
|
||
whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
|
||
}
|
||
if (whereClause) {
|
||
sql += ` WHERE ${whereClause}`;
|
||
}
|
||
}
|
||
return sql;
|
||
}
|
||
translateRemove(entity, operation, option) {
|
||
const { data, filter, sorter, indexFrom, count } = operation;
|
||
(0, assert_1.default)(!sorter, '当前remove不支持sorter行为');
|
||
// 使用结构化的JOIN分析
|
||
const { aliasDict, filterRefAlias, mainTable, mainAlias, joinInfos, currentNumber } = this.analyzeJoinStructured(entity, { filter, sorter });
|
||
// 构建过滤条件
|
||
const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, { includedDeleted: option?.includedDeleted });
|
||
const { attributes } = this.schema[entity];
|
||
if (option?.deletePhysically) {
|
||
// 物理删除
|
||
(0, assert_1.default)((0, lodash_1.difference)(Object.keys(data), [types_1.UpdateAtAttribute, types_1.DeleteAtAttribute]).length === 0);
|
||
let sql;
|
||
if (joinInfos.length === 0) {
|
||
// 单表删除
|
||
sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
|
||
if (filterText) {
|
||
sql += ` WHERE ${filterText}`;
|
||
}
|
||
}
|
||
else {
|
||
// 多表删除 - PostgreSQL语法: DELETE FROM main USING other_tables WHERE join_conditions AND filter
|
||
sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
|
||
// USING子句包含所有JOIN的表
|
||
const usingTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
|
||
sql += ` USING ${usingTables}`;
|
||
// WHERE子句包含JOIN条件和过滤条件
|
||
const joinConditions = this.buildJoinConditions(joinInfos);
|
||
let whereClause = joinConditions;
|
||
if (filterText) {
|
||
whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
|
||
}
|
||
if (whereClause) {
|
||
sql += ` WHERE ${whereClause}`;
|
||
}
|
||
}
|
||
return sql;
|
||
}
|
||
else {
|
||
// 软删除 - 实际是UPDATE操作
|
||
const setClauses = [];
|
||
for (const attr in data) {
|
||
(0, assert_1.default)([types_1.TriggerDataAttribute, types_1.TriggerUuidAttribute, types_1.DeleteAtAttribute, types_1.UpdateAtAttribute].includes(attr));
|
||
const value = this.translateAttrValue(attributes[attr].type, data[attr]);
|
||
setClauses.push(`"${attr}" = ${value}`);
|
||
}
|
||
const setClause = setClauses.join(', ');
|
||
let sql;
|
||
if (joinInfos.length === 0) {
|
||
// 单表更新
|
||
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
||
if (filterText) {
|
||
sql += ` WHERE ${filterText}`;
|
||
}
|
||
}
|
||
else {
|
||
// 多表更新
|
||
sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
|
||
const fromTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
|
||
sql += ` FROM ${fromTables}`;
|
||
const joinConditions = this.buildJoinConditions(joinInfos);
|
||
let whereClause = joinConditions;
|
||
if (filterText) {
|
||
whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
|
||
}
|
||
if (whereClause) {
|
||
sql += ` WHERE ${whereClause}`;
|
||
}
|
||
}
|
||
return sql;
|
||
}
|
||
}
|
||
/**
|
||
* PostgreSQL专用的结构化JOIN分析
|
||
* 返回结构化的JOIN信息,而不是拼接好的FROM字符串
|
||
*/
|
||
analyzeJoinStructured(entity, { filter, sorter }, initialNumber) {
|
||
const { schema } = this;
|
||
let number = initialNumber || 1;
|
||
const filterRefAlias = {};
|
||
const mainAlias = `${entity}_${number++}`;
|
||
const mainTable = this.getStorageName(entity);
|
||
const aliasDict = {
|
||
'./': mainAlias,
|
||
};
|
||
const joinInfos = [];
|
||
const analyzeFilterNode = ({ node, path, entityName, alias }) => {
|
||
Object.keys(node).forEach((op) => {
|
||
if (['$and', '$or'].includes(op)) {
|
||
node[op].forEach((subNode) => analyzeFilterNode({
|
||
node: subNode,
|
||
path,
|
||
entityName,
|
||
alias,
|
||
}));
|
||
}
|
||
else if (['$not'].includes(op)) {
|
||
analyzeFilterNode({
|
||
node: node[op],
|
||
path,
|
||
entityName,
|
||
alias,
|
||
});
|
||
}
|
||
else if (['$text'].includes(op)) {
|
||
// 全文搜索,不需要JOIN
|
||
}
|
||
else {
|
||
const rel = (0, relation_1.judgeRelation)(this.schema, entityName, op);
|
||
if (typeof rel === 'string') {
|
||
const pathAttr = `${path}${op}/`;
|
||
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||
const alias2 = `${rel}_${number++}`;
|
||
aliasDict[pathAttr] = alias2;
|
||
joinInfos.push({
|
||
table: this.getStorageName(rel),
|
||
alias: alias2,
|
||
leftAlias: alias,
|
||
leftKey: op + 'Id',
|
||
rightKey: 'id',
|
||
});
|
||
analyzeFilterNode({
|
||
node: node[op],
|
||
path: pathAttr,
|
||
entityName: rel,
|
||
alias: alias2,
|
||
});
|
||
}
|
||
else {
|
||
analyzeFilterNode({
|
||
node: node[op],
|
||
path: pathAttr,
|
||
entityName: rel,
|
||
alias: aliasDict[pathAttr],
|
||
});
|
||
}
|
||
}
|
||
else if (rel === 2) {
|
||
const pathAttr = `${path}${op}/`;
|
||
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||
const alias2 = `${op}_${number++}`;
|
||
aliasDict[pathAttr] = alias2;
|
||
joinInfos.push({
|
||
table: this.getStorageName(op),
|
||
alias: alias2,
|
||
leftAlias: alias,
|
||
leftKey: 'entityId',
|
||
rightKey: 'id',
|
||
extraCondition: `"${alias}"."entity" = '${op}'`,
|
||
});
|
||
analyzeFilterNode({
|
||
node: node[op],
|
||
path: pathAttr,
|
||
entityName: op,
|
||
alias: alias2,
|
||
});
|
||
}
|
||
else {
|
||
analyzeFilterNode({
|
||
node: node[op],
|
||
path: pathAttr,
|
||
entityName: op,
|
||
alias: aliasDict[pathAttr],
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
if (node['#id']) {
|
||
(0, assert_1.default)(!filterRefAlias[node['#id']]);
|
||
filterRefAlias[node['#id']] = [alias, entityName];
|
||
}
|
||
};
|
||
if (filter) {
|
||
analyzeFilterNode({
|
||
node: filter,
|
||
path: './',
|
||
entityName: entity,
|
||
alias: mainAlias,
|
||
});
|
||
}
|
||
// TODO: sorter的分析类似,这里省略(UPDATE/DELETE通常不需要sorter)
|
||
(0, assert_1.default)(!sorter, '当前analyzeJoinStructured不支持sorter行为');
|
||
return {
|
||
aliasDict,
|
||
filterRefAlias,
|
||
mainTable,
|
||
mainAlias,
|
||
joinInfos,
|
||
currentNumber: number,
|
||
};
|
||
}
|
||
/**
|
||
* 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
|
||
*/
|
||
buildJoinConditions(joinInfos) {
|
||
const conditions = [];
|
||
for (const join of joinInfos) {
|
||
let condition = `"${join.leftAlias}"."${join.leftKey}" = "${join.alias}"."${join.rightKey}"`;
|
||
if (join.extraCondition) {
|
||
condition = `(${condition} AND ${join.extraCondition})`;
|
||
}
|
||
conditions.push(condition);
|
||
}
|
||
return conditions.join(' AND ');
|
||
}
|
||
/**
|
||
* 生成 PostgreSQL UPSERT 语句
|
||
* INSERT ... ON CONFLICT (key) DO UPDATE SET ...
|
||
*/
|
||
translateUpsert(entity, data, conflictKeys, updateAttrs) {
|
||
const { schema } = this;
|
||
const { attributes, storageName = entity } = schema[entity];
|
||
// 基础 INSERT 语句
|
||
let sql = this.translateInsert(entity, data);
|
||
// ON CONFLICT 子句
|
||
const conflictColumns = conflictKeys.map(k => this.quoteIdentifier(k)).join(', ');
|
||
sql += ` ON CONFLICT (${conflictColumns})`;
|
||
// DO UPDATE SET 子句
|
||
const dataFull = data.reduce((prev, cur) => Object.assign({}, cur, prev), {});
|
||
const attrsToUpdate = updateAttrs || Object.keys(dataFull).filter(attr => attributes.hasOwnProperty(attr) && !conflictKeys.includes(attr) && attr !== 'id');
|
||
if (attrsToUpdate.length > 0) {
|
||
const updateParts = attrsToUpdate.map(attr => `${this.quoteIdentifier(attr)} = EXCLUDED.${this.quoteIdentifier(attr)}`);
|
||
sql += ` DO UPDATE SET ${updateParts.join(', ')}`;
|
||
}
|
||
else {
|
||
sql += ' DO NOTHING';
|
||
}
|
||
return sql;
|
||
}
|
||
populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, option) {
|
||
// 这个方法不应该被直接调用了,因为translateUpdate已经重写
|
||
throw new Error('populateUpdateStmt should not be called directly in PostgreSQL. Use translateUpdate instead.');
|
||
}
|
||
populateRemoveStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, option) {
|
||
// 这个方法不应该被直接调用了,因为translateRemove已经重写
|
||
throw new Error('populateRemoveStmt should not be called directly in PostgreSQL. Use translateRemove instead.');
|
||
}
|
||
/**
|
||
* 将 PostgreSQL 返回的 Type 回译成 oak 的类型,是 populateDataTypeDef 的反函数
|
||
* @param type PostgreSQL 类型字符串
|
||
*/
|
||
reTranslateToAttribute(type) {
|
||
// 处理带长度的类型:character varying(255), character(10)
|
||
const varcharMatch = /^character varying\((\d+)\)$/.exec(type);
|
||
if (varcharMatch) {
|
||
return {
|
||
type: 'varchar',
|
||
params: {
|
||
length: parseInt(varcharMatch[1], 10),
|
||
}
|
||
};
|
||
}
|
||
const charMatch = /^character\((\d+)\)$/.exec(type);
|
||
if (charMatch) {
|
||
return {
|
||
type: 'char',
|
||
params: {
|
||
length: parseInt(charMatch[1], 10),
|
||
}
|
||
};
|
||
}
|
||
// 处理带精度和小数位的类型:numeric(10,2)
|
||
const numericWithScaleMatch = /^numeric\((\d+),(\d+)\)$/.exec(type);
|
||
if (numericWithScaleMatch) {
|
||
return {
|
||
type: 'decimal',
|
||
params: {
|
||
precision: parseInt(numericWithScaleMatch[1], 10),
|
||
scale: parseInt(numericWithScaleMatch[2], 10),
|
||
},
|
||
};
|
||
}
|
||
// 处理只带精度的类型:numeric(10), timestamp(6)
|
||
const numericMatch = /^numeric\((\d+)\)$/.exec(type);
|
||
if (numericMatch) {
|
||
return {
|
||
type: 'decimal',
|
||
params: {
|
||
precision: parseInt(numericMatch[1], 10),
|
||
scale: 0,
|
||
},
|
||
};
|
||
}
|
||
const timestampMatch = /^timestamp\((\d+)\) without time zone$/.exec(type);
|
||
if (timestampMatch) {
|
||
return {
|
||
type: 'timestamp',
|
||
params: {
|
||
precision: parseInt(timestampMatch[1], 10),
|
||
},
|
||
};
|
||
}
|
||
const timeMatch = /^time\((\d+)\) without time zone$/.exec(type);
|
||
if (timeMatch) {
|
||
return {
|
||
type: 'time',
|
||
params: {
|
||
precision: parseInt(timeMatch[1], 10),
|
||
},
|
||
};
|
||
}
|
||
// PostgreSQL 类型映射到 oak 类型
|
||
const typeMap = {
|
||
'bigint': 'bigint',
|
||
'integer': 'integer',
|
||
'smallint': 'smallint',
|
||
'real': 'real',
|
||
'double precision': 'double precision',
|
||
'boolean': 'boolean',
|
||
'text': 'text',
|
||
'jsonb': 'object', // 框架使用 jsonb 存储 object/array
|
||
'json': 'object',
|
||
'bytea': 'bytea',
|
||
'character varying': 'varchar',
|
||
'character': 'char',
|
||
'timestamp without time zone': 'timestamp',
|
||
'time without time zone': 'time',
|
||
'date': 'date',
|
||
'uuid': 'uuid',
|
||
'geometry': 'geometry',
|
||
};
|
||
const mappedType = typeMap[type];
|
||
if (mappedType) {
|
||
return { type: mappedType };
|
||
}
|
||
// 如果是用户定义的枚举类型,返回 enum(具体值需要额外查询)
|
||
// 这里先返回基础类型,枚举值在 readSchema 中单独处理
|
||
return { type: type };
|
||
}
|
||
/**
|
||
* 从 PostgreSQL 数据库读取当前的 schema 结构
|
||
*/
|
||
async readSchema(execFn) {
|
||
const result = {};
|
||
// 1. 获取所有表
|
||
const tablesSql = `
|
||
SELECT tablename
|
||
FROM pg_tables
|
||
WHERE schemaname = 'public'
|
||
ORDER BY tablename;
|
||
`;
|
||
const [tablesResult] = await execFn(tablesSql);
|
||
for (const tableRow of tablesResult) {
|
||
const tableName = tableRow.tablename;
|
||
// 2. 获取表的列信息
|
||
const columnsSql = `
|
||
SELECT
|
||
column_name,
|
||
data_type,
|
||
character_maximum_length,
|
||
numeric_precision,
|
||
numeric_scale,
|
||
is_nullable,
|
||
column_default,
|
||
udt_name
|
||
FROM information_schema.columns
|
||
WHERE table_schema = 'public'
|
||
AND table_name = '${tableName}'
|
||
ORDER BY ordinal_position;
|
||
`;
|
||
const [columnsResult] = await execFn(columnsSql);
|
||
const attributes = {};
|
||
for (const col of columnsResult) {
|
||
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;
|
||
let attr;
|
||
// 处理用户定义类型(枚举)
|
||
if (dataType === 'USER-DEFINED') {
|
||
// 查询枚举值
|
||
const enumSql = `
|
||
SELECT e.enumlabel
|
||
FROM pg_type t
|
||
JOIN pg_enum e ON t.oid = e.enumtypid
|
||
WHERE t.typname = '${udtName}'
|
||
ORDER BY e.enumsortorder;
|
||
`;
|
||
const [enumResult] = await execFn(enumSql);
|
||
const enumeration = enumResult.map((r) => r.enumlabel);
|
||
attr = {
|
||
type: 'enum',
|
||
enumeration,
|
||
};
|
||
}
|
||
else {
|
||
// 构建完整的类型字符串
|
||
let fullType = dataType;
|
||
if (maxLength) {
|
||
fullType = `${dataType}(${maxLength})`;
|
||
}
|
||
else if (precision !== null && scale !== null) {
|
||
fullType = `${dataType}(${precision},${scale})`;
|
||
}
|
||
else if (precision !== null) {
|
||
fullType = `${dataType}(${precision})`;
|
||
}
|
||
attr = this.reTranslateToAttribute(fullType);
|
||
}
|
||
// 处理约束
|
||
attr.notNull = isNullable === 'NO';
|
||
// 处理默认值(简化处理,复杂的默认值可能需要更精细的解析)
|
||
if (defaultValue && !defaultValue.includes('nextval')) {
|
||
// 跳过序列默认值
|
||
// 简单处理:移除类型转换
|
||
let cleanDefault = defaultValue.replace(/::[a-z]+/gi, '').replace(/'/g, '');
|
||
if (cleanDefault === 'true') {
|
||
attr.default = true;
|
||
}
|
||
else if (cleanDefault === 'false') {
|
||
attr.default = false;
|
||
}
|
||
else if (!isNaN(Number(cleanDefault))) {
|
||
attr.default = Number(cleanDefault);
|
||
}
|
||
else {
|
||
attr.default = cleanDefault;
|
||
}
|
||
}
|
||
// 检查是否是序列列(IDENTITY)
|
||
if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) {
|
||
attr.sequenceStart = 10000; // 默认起始值
|
||
}
|
||
attributes[colName] = attr;
|
||
}
|
||
// 3. 获取索引信息
|
||
const indexesSql = `
|
||
SELECT
|
||
i.relname as index_name,
|
||
ix.indisunique as is_unique,
|
||
am.amname as index_type,
|
||
pg_get_indexdef(ix.indexrelid) as index_def
|
||
FROM pg_class t
|
||
JOIN pg_index ix ON t.oid = ix.indrelid
|
||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||
JOIN pg_am am ON i.relam = am.oid
|
||
WHERE t.relname = '${tableName}'
|
||
AND t.relkind = 'r'
|
||
AND i.relname NOT LIKE '%_pkey'
|
||
AND NOT ix.indisprimary
|
||
ORDER BY i.relname;
|
||
`;
|
||
const [indexesResult] = await execFn(indexesSql);
|
||
if (indexesResult.length > 0) {
|
||
const indexes = [];
|
||
for (const row of indexesResult) {
|
||
const { index_name: indexName, is_unique: isUnique, index_type: indexType, index_def: indexDef } = row;
|
||
// 解析索引定义以获取列名和配置
|
||
const index = {
|
||
name: indexName,
|
||
attributes: [],
|
||
};
|
||
// 解析索引定义字符串
|
||
// 示例: CREATE INDEX "user_index_fulltext_chinese" ON public."user" USING gin (to_tsvector('chinese'::regconfig, (COALESCE(name, ''::text) || ' '::text) || COALESCE(nickname, ''::text)))
|
||
if (indexType === 'gin' && indexDef.includes('to_tsvector')) {
|
||
// 全文索引
|
||
index.config = { type: 'fulltext' };
|
||
// 提取 tsConfig
|
||
const tsConfigMatch = indexDef.match(/to_tsvector\('([^']+)'/);
|
||
if (tsConfigMatch) {
|
||
const tsConfig = tsConfigMatch[1];
|
||
index.config.tsConfig = tsConfig;
|
||
}
|
||
// 提取列名(从 COALESCE 中)
|
||
const columnMatches = indexDef.matchAll(/COALESCE\("?([^",\s]+)"?/g);
|
||
const columns = Array.from(columnMatches, m => m[1]);
|
||
index.attributes = columns.map(col => ({ name: col }));
|
||
// 处理多语言索引的情况:移除语言后缀
|
||
// 例如: user_index_fulltext_chinese -> index_fulltext
|
||
const nameParts = indexName.split('_');
|
||
if (nameParts.length > 2) {
|
||
const possibleLang = nameParts[nameParts.length - 1];
|
||
// 如果最后一部分是语言代码,移除它
|
||
if (['chinese', 'english', 'simple', 'german', 'french', 'spanish', 'russian', 'japanese'].includes(possibleLang)) {
|
||
index.name = nameParts.slice(0, -1).join('_');
|
||
}
|
||
}
|
||
}
|
||
else if (indexType === 'gist') {
|
||
// 空间索引
|
||
index.config = { type: 'spatial' };
|
||
// 提取列名
|
||
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
||
if (columnMatch) {
|
||
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
||
index.attributes = columns.map(col => ({ name: col }));
|
||
}
|
||
}
|
||
else if (indexType === 'hash') {
|
||
// 哈希索引
|
||
index.config = { type: 'hash' };
|
||
// 提取列名
|
||
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
||
if (columnMatch) {
|
||
const columns = columnMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
|
||
index.attributes = columns.map(col => ({ name: col }));
|
||
}
|
||
}
|
||
else {
|
||
// B-tree 索引(默认)
|
||
// 提取列名和排序方向
|
||
const columnMatch = indexDef.match(/\(([^)]+)\)/);
|
||
if (columnMatch) {
|
||
const columnDefs = columnMatch[1].split(',');
|
||
index.attributes = columnDefs.map(colDef => {
|
||
const trimmed = colDef.trim().replace(/"/g, '');
|
||
const parts = trimmed.split(/\s+/);
|
||
const attr = { name: parts[0] };
|
||
// 检查排序方向
|
||
if (parts.includes('DESC')) {
|
||
attr.direction = 'DESC';
|
||
}
|
||
else if (parts.includes('ASC')) {
|
||
attr.direction = 'ASC';
|
||
}
|
||
return attr;
|
||
});
|
||
}
|
||
// 如果是唯一索引
|
||
if (isUnique) {
|
||
index.config = { unique: true };
|
||
}
|
||
}
|
||
// 移除表名前缀(如果存在)
|
||
// 例如: user_index_fulltext -> index_fulltext
|
||
if (index.name.startsWith(`${tableName}_`)) {
|
||
index.name = index.name.substring(tableName.length + 1);
|
||
}
|
||
indexes.push(index);
|
||
}
|
||
Object.assign(result, {
|
||
[tableName]: {
|
||
attributes,
|
||
indexes,
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
Object.assign(result, {
|
||
[tableName]: {
|
||
attributes,
|
||
}
|
||
});
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* 将属性定义转换为 PostgreSQL DDL 语句
|
||
* @param attr 属性名
|
||
* @param attrDef 属性定义
|
||
*/
|
||
translateAttributeDef(attr, attrDef) {
|
||
let sql = `"${attr}" `;
|
||
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
||
// 处理序列类型(IDENTITY)
|
||
if (typeof sequenceStart === 'number') {
|
||
sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart}) UNIQUE`;
|
||
return sql;
|
||
}
|
||
// 处理枚举类型
|
||
if (type === 'enum') {
|
||
(0, assert_1.default)(enumeration, 'Enum type requires enumeration values');
|
||
// 注意:这里返回的是占位符,实际的枚举类型名在 translateCreateEntity 中确定
|
||
// 为了比较一致性,我们使用枚举值的字符串表示
|
||
sql += `enum(${enumeration.map(v => `'${v}'`).join(',')})`;
|
||
}
|
||
else {
|
||
sql += this.populateDataTypeDef(type, params, enumeration);
|
||
}
|
||
// NOT NULL 约束
|
||
if (notNull || type === 'geometry') {
|
||
sql += ' NOT NULL';
|
||
}
|
||
// UNIQUE 约束
|
||
if (unique) {
|
||
sql += ' UNIQUE';
|
||
}
|
||
// 默认值
|
||
if (defaultValue !== undefined && !sequenceStart) {
|
||
(0, assert_1.default)(type !== 'ref', 'ref type should not have default value');
|
||
sql += ` DEFAULT ${this.translateAttrValue(type, defaultValue)}`;
|
||
}
|
||
// 主键
|
||
if (attr === 'id') {
|
||
sql += ' PRIMARY KEY';
|
||
}
|
||
return sql;
|
||
}
|
||
/**
|
||
* 比较两个 SQL 语句是否等价(用于 schema diff)
|
||
* 忽略空格、大小写等格式差异
|
||
*/
|
||
compareSql(sql1, sql2) {
|
||
// 标准化 SQL:移除多余空格,统一大小写
|
||
const normalize = (sql) => {
|
||
return sql
|
||
.replace(/\s+/g, ' ') // 多个空格合并为一个
|
||
.replace(/\(\s+/g, '(') // 移除括号后的空格
|
||
.replace(/\s+\)/g, ')') // 移除括号前的空格
|
||
.replace(/,\s+/g, ',') // 移除逗号后的空格
|
||
.trim()
|
||
.toLowerCase();
|
||
};
|
||
return normalize(sql1) === normalize(sql2);
|
||
}
|
||
}
|
||
exports.PostgreSQLTranslator = PostgreSQLTranslator;
|