Compare commits

...

3 Commits

6 changed files with 2347 additions and 105 deletions

View File

@ -523,17 +523,13 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
stmt2 += ' AND ';
}
if (Array.isArray(value)) {
// 如果是数组,检查是否有任意元素匹配
if (p) {
const accessor = this.buildJsonAccessor(columnRef, p, false);
// 使用 jsonb 数组重叠检查
stmt2 += `(${accessor} ?| array[${value.map(v => `'${v}'`).join(', ')}])`;
}
else {
stmt2 += `(${columnRef} ?| array[${value.map((v) => `'${v}'`).join(', ')}])`;
}
// 如果是数组,检查是否有任意元素匹配
const accessor = p ? this.buildJsonAccessor(columnRef, p, false) : columnRef;
// 使用 @> 操作符检查数组是否包含任意一个值(支持数字和字符串)
const conditions = value.map(v => `${accessor} @> '${JSON.stringify(v)}'::jsonb`);
stmt2 += `(${conditions.join(' OR ')})`;
}
else {
else if (typeof value === 'object' && value !== null) {
// 对象重叠检查 - 检查是否有共同的键
const keys = Object.keys(value);
if (p) {
@ -544,6 +540,17 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
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
@ -570,17 +577,40 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
}
}
else if (attr2 === '$exists') {
// 检查键是否存在
if (stmt2) {
stmt2 += ' AND ';
}
const keyToCheck = o[attr2];
if (p) {
const accessor = this.buildJsonAccessor(columnRef, p, false);
stmt2 += `(${accessor} ? '${keyToCheck}')`;
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 {
stmt2 += `(${columnRef} ? '${keyToCheck}')`;
// $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('$')) {

View File

@ -638,7 +638,7 @@ class SqlTranslator {
filter: filter2[attr]
}, currentNumber, filterRefAlias, option);
currentNumber = ct2;
whereText += `(${refAlia2}.id ${predicate} (${stmt}))`;
whereText += `(${this.quoteIdentifier(alias)}.${this.quoteIdentifier('id')} ${predicate} (${stmt}))`;
}
else {
/**

View File

@ -46,7 +46,7 @@ const GeoTypes = [
function transformGeoData(data: Geo): string {
if (data instanceof Array) {
const element = data[0];
if (element instanceof Array) {
// GeometryCollection
return `ST_GeomFromText('GEOMETRYCOLLECTION(${data.map(
@ -55,43 +55,43 @@ function transformGeoData(data: Geo): string {
} 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 as [number, number]).join(' ')})')`;
}
if (type === 'path') {
// LineString: coordinate 是 [[x1,y1], [x2,y2], ...]
const points = (coordinate as [number, number][])
@ -99,7 +99,7 @@ function transformGeoData(data: Geo): string {
.join(',');
return `ST_GeomFromText('LINESTRING(${points})')`;
}
if (type === 'polygon') {
// Polygon: coordinate 是 [[[x1,y1], [x2,y2], ...], [...]](外环和内环)
const rings = (coordinate as [number, number][][])
@ -107,7 +107,7 @@ function transformGeoData(data: Geo): string {
.join(',');
return `ST_GeomFromText('POLYGON(${rings})')`;
}
throw new Error(`Unsupported geometry type: ${type}`);
}
}
@ -468,16 +468,16 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
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
@ -488,11 +488,11 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
.replace(/\\/g, '\\\\') // 反斜杠必须先处理
.replace(/%/g, '\\%') // 百分号
.replace(/_/g, '\\_'); // 下划线
// 再进行字符串值转义
return escaped.replace(/'/g, "''");
}
/**
* PostgreSQL
*
@ -596,15 +596,13 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
stmt2 += ' AND ';
}
if (Array.isArray(value)) {
// 如果是数组,检查是否有任意元素匹配
if (p) {
const accessor = this.buildJsonAccessor(columnRef, p, false);
// 使用 jsonb 数组重叠检查
stmt2 += `(${accessor} ?| array[${value.map(v => `'${v}'`).join(', ')}])`;
} else {
stmt2 += `(${columnRef} ?| array[${value.map((v: any) => `'${v}'`).join(', ')}])`;
}
} else {
// 如果是数组,检查是否有任意元素匹配
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) {
@ -613,6 +611,15 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
} else {
stmt2 += `(${columnRef} ?| array[${keys.map(k => `'${k}'`).join(', ')}])`;
}
} else {
assert(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
@ -638,16 +645,36 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
stmt2 += `(${lengthExpr} ${this.translatePredicate(op, length[op])})`;
}
} else if (attr2 === '$exists') {
// 检查键是否存在
if (stmt2) {
stmt2 += ' AND ';
}
const keyToCheck = o[attr2];
if (p) {
const accessor = this.buildJsonAccessor(columnRef, p, false);
stmt2 += `(${accessor} ? '${keyToCheck}')`;
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 {
stmt2 += `(${columnRef} ? '${keyToCheck}')`;
// $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 等
@ -801,17 +828,17 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
const { config } = ele;
return config && config.type === 'fulltext';
});
assert(ftIndex, `Entity ${String(entity)} does not have a fulltext index`);
const { attributes } = ftIndex;
// PostgreSQL 全文搜索使用 to_tsvector 和 to_tsquery
// 将多个列合并成一个文档
const columns = attributes.map(({ name }) =>
const columns = attributes.map(({ name }) =>
`COALESCE("${alias}"."${name as string}", '')`
).join(" || ' ' || ");
// 处理搜索词:将空格分隔的词转换为 & 连接
const searchTerms = $search
.trim()
@ -826,7 +853,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
console.error('Full-text search: no valid search terms after escaping, returning FALSE condition');
return 'FALSE';
}
// 在 translateFullTextSearch 中使用
const pgLanguage = $language ? this.languageConfigMap[$language] : undefined;
@ -1073,7 +1100,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
// PostgreSQL 使用 POWER
return 'POWER(%s, %s)';
}
// ========== 比较运算 ==========
case '$gt': {
assert(argumentNumber === 2);
@ -1099,7 +1126,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
assert(argumentNumber === 2);
return '%s <> %s';
}
// ========== 字符串操作 ==========
case '$startsWith': {
assert(argumentNumber === 2);
@ -1123,7 +1150,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
result += ')';
return result;
}
// ========== 布尔运算 ==========
case '$true': {
return '%s = TRUE';
@ -1154,7 +1181,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
case '$not': {
return 'NOT %s';
}
// ========== 日期时间函数 ==========
case '$year': {
return 'EXTRACT(YEAR FROM %s)::integer';
@ -1209,7 +1236,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
// 参数: date, amount, unit
return '(%s + INTERVAL \'1 %s\' * %s)';
}
// ========== 地理空间函数 (PostGIS) ==========
case '$contains': {
assert(argumentNumber === 2);
@ -1227,7 +1254,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
assert(argumentNumber === 2);
return 'ST_Intersects(%s, %s)';
}
default: {
throw new Error(`unrecognized function ${fnName}`);
}
@ -1237,26 +1264,26 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
private translateAttrInExpression<T extends keyof ED>(entity: T, attr: string, exprText: string): string {
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;
}
protected translateExpression<T extends keyof ED>(
entity: T,
alias: string,
expression: RefOrExpression<keyof ED[T]["OpSchema"]>,
entity: T,
alias: string,
expression: RefOrExpression<keyof ED[T]["OpSchema"]>,
refDict: Record<string, [string, keyof ED]>
): string {
const translateConstant = (constant: number | string | Date): string => {
@ -1271,11 +1298,11 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
return `${constant}`;
}
};
const translateInner = (expr: any): string => {
const k = Object.keys(expr);
let result: string;
if (k.includes('#attr')) {
const attrName = (expr)['#attr'];
const attrText = `"${alias}"."${attrName}"`;
@ -1292,11 +1319,11 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
assert(k.length === 1);
const fnKey = k[0];
const fnArgs = (expr)[fnKey];
if (fnArgs instanceof Array) {
const fnName = this.translateFnName(fnKey, fnArgs.length);
const args: string[] = [fnName];
args.push(...fnArgs.map((ele: any) => {
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
return translateConstant(ele);
@ -1309,7 +1336,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
} else {
const fnName = this.translateFnName(fnKey, 1);
const args: string[] = [fnName];
if (['string', 'number'].includes(typeof fnArgs) || fnArgs instanceof Date) {
args.push(translateConstant(fnArgs));
} else {
@ -1319,7 +1346,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
result = format.apply(null, args);
}
}
return result;
};
@ -1338,19 +1365,19 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
option?: PostgreSQLSelectOption
): string {
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}`;
@ -1358,7 +1385,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
if (typeof indexFrom === 'number' && indexFrom > 0) {
sql += ` OFFSET ${indexFrom}`;
}
// FOR UPDATE 锁定
if (option?.forUpdate) {
sql += ' FOR UPDATE';
@ -1444,11 +1471,11 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
// 构建过滤条件
const { stmt: filterText } = this.translateFilter(
entity,
filter,
aliasDict,
filterRefAlias,
currentNumber,
entity,
filter,
aliasDict,
filterRefAlias,
currentNumber,
{ includedDeleted: option?.includedDeleted } as OP
);
@ -1693,7 +1720,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
extraCondition?: string;
}>): string {
const conditions: string[] = [];
for (const join of joinInfos) {
let condition = `"${join.leftAlias}"."${join.leftKey}" = "${join.alias}"."${join.rightKey}"`;
if (join.extraCondition) {
@ -1701,7 +1728,7 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
}
conditions.push(condition);
}
return conditions.join(' AND ');
}
@ -1717,29 +1744,29 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
): string {
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 =>
const updateParts = attrsToUpdate.map(attr =>
`${this.quoteIdentifier(attr)} = EXCLUDED.${this.quoteIdentifier(attr)}`
);
sql += ` DO UPDATE SET ${updateParts.join(', ')}`;
} else {
sql += ' DO NOTHING';
}
return sql;
}

View File

@ -812,7 +812,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
}, currentNumber, filterRefAlias, option);
currentNumber = ct2;
whereText += `(${refAlia2}.id ${predicate} (${stmt}))`;
whereText += `(${this.quoteIdentifier(alias)}.${this.quoteIdentifier('id')} ${predicate} (${stmt}))`;
}
else {
/**

View File

@ -880,9 +880,6 @@ describe('test mysqlstore', function () {
port: '',
},
},
dangerousVersions: [],
warningVersions: [],
soaVersion: '',
}
},
{
@ -1233,9 +1230,6 @@ describe('test mysqlstore', function () {
port: '',
},
},
dangerousVersions: [],
warningVersions: [],
soaVersion: '',
}
},
{
@ -1284,9 +1278,6 @@ describe('test mysqlstore', function () {
port: '',
},
},
dangerousVersions: [],
warningVersions: [],
soaVersion: '',
}
},
{
@ -1306,9 +1297,6 @@ describe('test mysqlstore', function () {
port: '',
},
},
dangerousVersions: [],
warningVersions: [],
soaVersion: '',
}
}]
}

2197
test/testPostgresStore.ts Normal file

File diff suppressed because it is too large Load Diff