oak-db/lib/PostgreSQL/translator.js

2140 lines
88 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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;