"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]; 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', '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', 'numeric': 'decimal', }; 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; const integerTypes = ['bigint', 'integer', 'smallint', 'serial', 'bigserial', 'smallserial']; if (maxLength && !integerTypes.includes(dataType)) { fullType = `${dataType}(${maxLength})`; } else if (precision !== null && scale !== null && !integerTypes.includes(dataType)) { fullType = `${dataType}(${precision},${scale})`; } else if (precision !== null && !integerTypes.includes(dataType)) { fullType = `${dataType}(${precision})`; } attr = this.reTranslateToAttribute(fullType); } // ========== 类型还原逻辑 ========== // 框架将某些语义类型存储为 bigint,需要根据列名还原 if (attr.type === 'bigint') { // 1. 检查是否是序列列 if (colName === '$$seq$$' || (defaultValue && defaultValue.includes('nextval'))) { attr.type = 'sequence'; attr.sequenceStart = 10000; // 默认起始值 } // 2. 检查是否是时间戳列 else if (['$$createAt$$', '$$updateAt$$', '$$deleteAt$$'].includes(colName)) { attr.type = 'datetime'; } // 3. 检查其他可能的时间类型列(根据命名约定) else if (colName.endsWith('At') || colName.endsWith('Time')) { // 可选:根据业务约定判断是否应该是 datetime // 这里保守处理,只转换框架标准字段 } } // 处理约束 - 只在为 true 时添加 if (isNullable === 'NO') { attr.notNull = true; } // 处理默认值 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 if (cleanDefault !== '') { attr.default = cleanDefault; } } // 检查唯一约束 const uniqueSql = ` SELECT COUNT(*) as cnt FROM pg_index ix JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) WHERE t.relname = '${tableName}' AND a.attname = '${colName}' AND ix.indisunique = true AND NOT ix.indisprimary AND array_length(ix.indkey, 1) = 1; `; const [uniqueResult] = await execFn(uniqueSql); const uniqueCount = parseInt(uniqueResult[0]?.cnt || '0', 10); if (uniqueCount > 0) { attr.unique = true; } 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 (type === 'sequence' || (typeof sequenceStart === 'number')) { sql += `bigint GENERATED BY DEFAULT AS IDENTITY (START WITH ${sequenceStart || 10000}) UNIQUE`; return sql; } // 处理枚举类型 if (type === 'enum') { (0, assert_1.default)(enumeration, 'Enum type requires enumeration values'); 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;