merge
This commit is contained in:
commit
e9059b8514
|
|
@ -115,4 +115,7 @@ dist
|
|||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
package-lock.json
|
||||
test/test-app-domain
|
||||
test/test-app-domain
|
||||
|
||||
|
||||
test-dist
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult } from 'oak-domain/lib/types';
|
||||
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult, Attribute, Index } from 'oak-domain/lib/types';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { MySQLConfiguration } from './types/Configuration';
|
||||
import { MySqlConnector } from './connector';
|
||||
import { MySqlTranslator, MySqlSelectOption, MysqlOperateOption } from './translator';
|
||||
import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { CreateEntityOption } from '../types/Translator';
|
||||
export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements AsyncRowStore<ED, Cxt> {
|
||||
import { DbStore } from '../types/dbStore';
|
||||
export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
||||
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
|
||||
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
|
||||
protected selectAbjointRow<T extends keyof ED, OP extends SelectOption>(entity: T, selection: ED[T]['Selection'], context: SyncContext<ED>, option: OP): Partial<ED[T]['Schema']>[];
|
||||
|
|
@ -31,7 +32,32 @@ export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt exte
|
|||
begin(option?: TxnOption): Promise<string>;
|
||||
commit(txnId: string): Promise<void>;
|
||||
rollback(txnId: string): Promise<void>;
|
||||
connect(): void;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
initialize(option: CreateEntityOption): Promise<void>;
|
||||
readSchema(): Promise<StorageSchema<ED>>;
|
||||
/**
|
||||
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
||||
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
||||
*/
|
||||
makeUpgradePlan(): Promise<Plan>;
|
||||
/**
|
||||
* 比较两个schema的不同,这里计算的是new对old的增量
|
||||
* @param schemaOld
|
||||
* @param SchemaNew
|
||||
*/
|
||||
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
||||
}
|
||||
type Plan = {
|
||||
newTables: Record<string, {
|
||||
attributes: Record<string, Attribute>;
|
||||
}>;
|
||||
newIndexes: Record<string, Index<any>[]>;
|
||||
updatedTables: Record<string, {
|
||||
attributes: Record<string, Attribute & {
|
||||
isNew: boolean;
|
||||
}>;
|
||||
}>;
|
||||
updatedIndexes: Record<string, Index<any>[]>;
|
||||
};
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
|
|||
// 边界,如果是toModi的对象,这里的外键确实有可能为空
|
||||
// assert(schema[e].toModi || r[`${attr}Id`] === r[attr].id, `对象${<string>e}取数据时,发现其外键与连接的对象的主键不一致,rowId是${r.id},其${attr}Id值为${r[`${attr}Id`]},连接的对象的主键为${r[attr].id}`);
|
||||
if (r[attr].id === null) {
|
||||
(0, assert_1.default)(schema[e].toModi || r[`${attr}Id`] === null);
|
||||
(0, assert_1.default)(schema[e].toModi || r[`${attr}Id`] === null, `对象${String(e)}取数据时,发现其外键找不到目标对象,rowId是${r.id},其外键${attr}Id值为${r[`${attr}Id`]}`);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
|
|
@ -289,8 +289,8 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
|
|||
async rollback(txnId) {
|
||||
await this.connector.rollbackTransaction(txnId);
|
||||
}
|
||||
connect() {
|
||||
this.connector.connect();
|
||||
async connect() {
|
||||
await this.connector.connect();
|
||||
}
|
||||
async disconnect() {
|
||||
await this.connector.disconnect();
|
||||
|
|
@ -304,5 +304,127 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
// 从数据库中读取当前schema
|
||||
readSchema() {
|
||||
return this.translator.readSchema((sql) => this.connector.exec(sql));
|
||||
}
|
||||
/**
|
||||
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
||||
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
||||
*/
|
||||
async makeUpgradePlan() {
|
||||
const originSchema = await this.readSchema();
|
||||
const plan = this.diffSchema(originSchema, this.translator.schema);
|
||||
return plan;
|
||||
}
|
||||
/**
|
||||
* 比较两个schema的不同,这里计算的是new对old的增量
|
||||
* @param schemaOld
|
||||
* @param SchemaNew
|
||||
*/
|
||||
diffSchema(schemaOld, schemaNew) {
|
||||
const plan = {
|
||||
newTables: {},
|
||||
newIndexes: {},
|
||||
updatedIndexes: {},
|
||||
updatedTables: {},
|
||||
};
|
||||
for (const table in schemaNew) {
|
||||
// mysql数据字典不分大小写的
|
||||
if (schemaOld[table] || schemaOld[table.toLowerCase()]) {
|
||||
const { attributes, indexes } = schemaOld[table] || schemaOld[table.toLowerCase()];
|
||||
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
|
||||
const assignToUpdateTables = (attr, isNew) => {
|
||||
if (!plan.updatedTables[table]) {
|
||||
plan.updatedTables[table] = {
|
||||
attributes: {
|
||||
[attr]: {
|
||||
...attributesNew[attr],
|
||||
isNew,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
plan.updatedTables[table].attributes[attr] = {
|
||||
...attributesNew[attr],
|
||||
isNew,
|
||||
};
|
||||
}
|
||||
};
|
||||
for (const attr in attributesNew) {
|
||||
if (attributes[attr]) {
|
||||
// 因为反向无法复原原来定义的attribute类型,这里就比较两次创建的sql是不是一致。
|
||||
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
||||
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
||||
if (!this.translator.compareSql(sql1, sql2)) {
|
||||
assignToUpdateTables(attr, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
assignToUpdateTables(attr, true);
|
||||
}
|
||||
}
|
||||
if (indexesNew) {
|
||||
const assignToIndexes = (index, isNew) => {
|
||||
if (isNew) {
|
||||
if (plan.newIndexes[table]) {
|
||||
plan.newIndexes[table].push(index);
|
||||
}
|
||||
else {
|
||||
plan.newIndexes[table] = [index];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (plan.updatedIndexes[table]) {
|
||||
plan.updatedIndexes[table].push(index);
|
||||
}
|
||||
else {
|
||||
plan.updatedIndexes[table] = [index];
|
||||
}
|
||||
}
|
||||
};
|
||||
const compareConfig = (config1, config2) => {
|
||||
const unique1 = config1?.unique || false;
|
||||
const unique2 = config2?.unique || false;
|
||||
if (unique1 !== unique2) {
|
||||
return false;
|
||||
}
|
||||
const type1 = config1?.type || 'btree';
|
||||
const type2 = config2?.type || 'btree';
|
||||
// parser目前无法从mysql中读出来,所以不比了
|
||||
return type1 === type2;
|
||||
};
|
||||
for (const index of indexesNew) {
|
||||
const { name, config, attributes } = index;
|
||||
const origin = indexes?.find(ele => ele.name === name);
|
||||
if (origin) {
|
||||
if (JSON.stringify(attributes) !== JSON.stringify(origin.attributes)) {
|
||||
// todo,这里要细致比较,不能用json.stringify
|
||||
assignToIndexes(index, false);
|
||||
}
|
||||
else {
|
||||
if (!compareConfig(config, origin.config)) {
|
||||
assignToIndexes(index, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
assignToIndexes(index, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
plan.newTables[table] = {
|
||||
attributes: schemaNew[table].attributes,
|
||||
};
|
||||
if (schemaNew[table].indexes) {
|
||||
plan.newIndexes[table] = schemaNew[table].indexes;
|
||||
}
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
exports.MysqlStore = MysqlStore;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema } from "oak-domain/lib/types";
|
||||
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Attribute } from "oak-domain/lib/types";
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
|
||||
import { SqlOperateOption, SqlSelectOption, SqlTranslator } from "../sqlTranslator";
|
||||
|
|
@ -97,6 +97,7 @@ export declare class MySqlTranslator<ED extends EntityDict & BaseEntityDict> ext
|
|||
protected translateObjectProjection(projection: Record<string, any>, alias: string, attr: string, prefix: string): string;
|
||||
protected translateAttrValue(dataType: DataType | Ref, value: any): string;
|
||||
protected translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string;
|
||||
translateAttributeDef(attr: string, attrDef: Attribute): string;
|
||||
translateCreateEntity<T extends keyof ED>(entity: T, options?: CreateEntityOption): string[];
|
||||
private translateFnName;
|
||||
private translateAttrInExpression;
|
||||
|
|
@ -104,4 +105,10 @@ export declare class MySqlTranslator<ED extends EntityDict & BaseEntityDict> ext
|
|||
protected populateSelectStmt<T extends keyof ED>(projectionText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, groupByText?: string, indexFrom?: number, count?: number, option?: MySqlSelectOption): string;
|
||||
protected populateUpdateStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: MysqlOperateOption): string;
|
||||
protected populateRemoveStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: MysqlOperateOption): string;
|
||||
/**
|
||||
* 将MySQL返回的Type回译成oak的类型,是 populateDataTypeDef 的反函数
|
||||
* @param type
|
||||
*/
|
||||
private reTranslateToAttribute;
|
||||
readSchema(execFn: (sql: string) => Promise<any>): Promise<StorageSchema<ED>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,21 +119,21 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
// numeric types
|
||||
"bit",
|
||||
"int",
|
||||
"integer",
|
||||
"integer", // synonym for int
|
||||
"tinyint",
|
||||
"smallint",
|
||||
"mediumint",
|
||||
"bigint",
|
||||
"float",
|
||||
"double",
|
||||
"double precision",
|
||||
"real",
|
||||
"double precision", // synonym for double
|
||||
"real", // synonym for double
|
||||
"decimal",
|
||||
"dec",
|
||||
"numeric",
|
||||
"fixed",
|
||||
"bool",
|
||||
"boolean",
|
||||
"dec", // synonym for decimal
|
||||
"numeric", // synonym for decimal
|
||||
"fixed", // synonym for decimal
|
||||
"bool", // synonym for tinyint
|
||||
"boolean", // synonym for tinyint
|
||||
// date and time types
|
||||
"date",
|
||||
"datetime",
|
||||
|
|
@ -142,10 +142,10 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
"year",
|
||||
// string types
|
||||
"char",
|
||||
"nchar",
|
||||
"nchar", // synonym for national char
|
||||
"national char",
|
||||
"varchar",
|
||||
"nvarchar",
|
||||
"nvarchar", // synonym for national varchar
|
||||
"national varchar",
|
||||
"blob",
|
||||
"text",
|
||||
|
|
@ -266,14 +266,18 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
return 'text ';
|
||||
}
|
||||
if (type === 'ref') {
|
||||
return 'char(36)';
|
||||
return 'char(36) ';
|
||||
}
|
||||
if (['bool', 'boolean'].includes(type)) {
|
||||
// MySQL读出来就是tinyint(1)
|
||||
return 'tinyint(1) ';
|
||||
}
|
||||
if (type === 'money') {
|
||||
return 'bigint';
|
||||
return 'bigint ';
|
||||
}
|
||||
if (type === 'enum') {
|
||||
(0, assert_1.default)(enumeration);
|
||||
return `enum(${enumeration.map(ele => `'${ele}'`).join(',')})`;
|
||||
return `enum(${enumeration.map(ele => `'${ele}'`).join(',')}) `;
|
||||
}
|
||||
if (MySqlTranslator.withLengthDataTypes.includes(type)) {
|
||||
if (params) {
|
||||
|
|
@ -291,34 +295,34 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
if (typeof scale === 'number') {
|
||||
return `${type}(${precision}, ${scale}) `;
|
||||
}
|
||||
return `${type}(${precision})`;
|
||||
return `${type}(${precision}) `;
|
||||
}
|
||||
else {
|
||||
const { precision, scale } = MySqlTranslator.dataTypeDefaults[type];
|
||||
if (typeof scale === 'number') {
|
||||
return `${type}(${precision}, ${scale}) `;
|
||||
}
|
||||
return `${type}(${precision})`;
|
||||
return `${type}(${precision}) `;
|
||||
}
|
||||
}
|
||||
if (MySqlTranslator.withWidthDataTypes.includes(type)) {
|
||||
(0, assert_1.default)(type === 'int');
|
||||
const { width } = params;
|
||||
const { width } = params || { width: 4 };
|
||||
switch (width) {
|
||||
case 1: {
|
||||
return 'tinyint';
|
||||
return 'tinyint ';
|
||||
}
|
||||
case 2: {
|
||||
return 'smallint';
|
||||
return 'smallint ';
|
||||
}
|
||||
case 3: {
|
||||
return 'mediumint';
|
||||
return 'mediumint ';
|
||||
}
|
||||
case 4: {
|
||||
return 'int';
|
||||
return 'int ';
|
||||
}
|
||||
default: {
|
||||
return 'bigint';
|
||||
return 'bigint ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -553,6 +557,28 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
const columns2 = attributes.map(({ name }) => `${alias}.${name}`);
|
||||
return ` match(${columns2.join(',')}) against ('${$search}' in natural language mode)`;
|
||||
}
|
||||
translateAttributeDef(attr, attrDef) {
|
||||
let sql = `\`${attr}\` `;
|
||||
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
||||
sql += this.populateDataTypeDef(type, params, enumeration);
|
||||
if (notNull || type === 'geometry') {
|
||||
sql += ' not null ';
|
||||
}
|
||||
if (unique) {
|
||||
sql += ' unique ';
|
||||
}
|
||||
if (typeof sequenceStart === 'number') {
|
||||
sql += ' auto_increment unique ';
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
(0, assert_1.default)(type !== 'ref');
|
||||
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
|
||||
}
|
||||
if (attr === types_1.PrimaryKeyAttribute) {
|
||||
sql += ' primary key';
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
translateCreateEntity(entity, options) {
|
||||
const ifExists = options?.ifExists || 'drop';
|
||||
const { schema } = this;
|
||||
|
|
@ -578,32 +604,14 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
sql += '(';
|
||||
// 翻译所有的属性
|
||||
Object.keys(attributes).forEach((attr, idx) => {
|
||||
const attrDef = attributes[attr];
|
||||
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
|
||||
sql += `\`${attr}\` `;
|
||||
sql += this.populateDataTypeDef(type, params, enumeration);
|
||||
if (notNull || type === 'geometry') {
|
||||
sql += ' not null ';
|
||||
const attrSql = this.translateAttributeDef(attr, attributes[attr]);
|
||||
if (idx !== 0) {
|
||||
sql += ', ';
|
||||
}
|
||||
if (unique) {
|
||||
sql += ' unique ';
|
||||
}
|
||||
if (sequenceStart) {
|
||||
if (hasSequence) {
|
||||
throw new Error(`「${entity}」只能有一个sequence列`);
|
||||
}
|
||||
hasSequence = sequenceStart;
|
||||
sql += ' auto_increment unique ';
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
(0, assert_1.default)(type !== 'ref');
|
||||
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
|
||||
}
|
||||
if (attr === 'id') {
|
||||
sql += ' primary key';
|
||||
}
|
||||
if (idx < Object.keys(attributes).length - 1) {
|
||||
sql += ',\n';
|
||||
sql += attrSql;
|
||||
if (typeof attributes[attr].sequenceStart === 'number') {
|
||||
(0, assert_1.default)(hasSequence === false, 'Entity can only have one auto increment attribute.');
|
||||
hasSequence = attributes[attr].sequenceStart;
|
||||
}
|
||||
});
|
||||
// 翻译索引信息
|
||||
|
|
@ -621,12 +629,11 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
else if (type === 'spatial') {
|
||||
sql += ' spatial ';
|
||||
}
|
||||
sql += `index ${name} `;
|
||||
sql += `index \`${name}\` `;
|
||||
if (type === 'hash') {
|
||||
sql += ` using hash `;
|
||||
}
|
||||
sql += '(';
|
||||
let includeDeleteAt = false;
|
||||
attributes.forEach(({ name, size, direction }, idx2) => {
|
||||
sql += `\`${name}\``;
|
||||
if (size) {
|
||||
|
|
@ -636,15 +643,9 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
sql += ` ${direction}`;
|
||||
}
|
||||
if (idx2 < attributes.length - 1) {
|
||||
sql += ',';
|
||||
}
|
||||
if (name === '$$deleteAt$$') {
|
||||
includeDeleteAt = true;
|
||||
sql += ', ';
|
||||
}
|
||||
});
|
||||
if (!includeDeleteAt && !type) {
|
||||
sql += ', `$$deleteAt$$`'; // 在mysql80+之后,需要给属性加上``包裹,否则会报错
|
||||
}
|
||||
sql += ')';
|
||||
if (parser) {
|
||||
sql += ` with parser ${parser}`;
|
||||
|
|
@ -796,10 +797,11 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
case '$dayOfYear': {
|
||||
return 'DAYOFYEAR(%s)';
|
||||
}
|
||||
case '$dateDiff': {
|
||||
(0, assert_1.default)(argumentNumber === 3);
|
||||
return 'DATEDIFF(%s, %s, %s)';
|
||||
}
|
||||
// 这个实现有问题,DATEDIFF只是计算两个日期之间的天数差,只接受两个参数,放在translateExperession里实现
|
||||
// case '$dateDiff': {
|
||||
// assert(argumentNumber === 3);
|
||||
// return 'DATEDIFF(%s, %s, %s)';
|
||||
// }
|
||||
case '$contains': {
|
||||
(0, assert_1.default)(argumentNumber === 2);
|
||||
return 'ST_CONTAINS(%s, %s)';
|
||||
|
|
@ -816,6 +818,22 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
result += ')';
|
||||
return result;
|
||||
}
|
||||
// ========== 聚合函数 ==========
|
||||
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(`unrecoganized function ${fnName}`);
|
||||
}
|
||||
|
|
@ -859,10 +877,135 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
}
|
||||
else {
|
||||
(0, assert_1.default)(k.length === 1);
|
||||
if ((expr)[k[0]] instanceof Array) {
|
||||
const fnName = this.translateFnName(k[0], (expr)[k[0]].length);
|
||||
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 `FROM_UNIXTIME(${arg.valueOf()} / 1000)`;
|
||||
}
|
||||
else if (typeof arg === 'number') {
|
||||
return `FROM_UNIXTIME(${arg} / 1000)`;
|
||||
}
|
||||
else {
|
||||
return translateInner(arg);
|
||||
}
|
||||
};
|
||||
const date1Str = translateDateArg(date1Expr);
|
||||
const date2Str = translateDateArg(date2Expr);
|
||||
// MySQL TIMESTAMPDIFF 单位映射
|
||||
const unitMap = {
|
||||
's': 'SECOND',
|
||||
'm': 'MINUTE',
|
||||
'h': 'HOUR',
|
||||
'd': 'DAY',
|
||||
'M': 'MONTH',
|
||||
'y': 'YEAR'
|
||||
};
|
||||
const mysqlUnit = unitMap[unit];
|
||||
if (!mysqlUnit) {
|
||||
throw new Error(`Unsupported date diff unit: ${unit}`);
|
||||
}
|
||||
// TIMESTAMPDIFF(unit, date1, date2) 返回 date2 - date1
|
||||
// 但类型定义是 date1 - date2,所以参数顺序要反过来
|
||||
result = `TIMESTAMPDIFF(${mysqlUnit}, ${date2Str}, ${date1Str})`;
|
||||
}
|
||||
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 向上取整
|
||||
result = `CEIL(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(typeof unit === 'string', 'unit should be string');
|
||||
console.warn('暂不支持 $dateCeil 对月年单位的处理');
|
||||
throw new Error(`Unsupported date ceil unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
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(timestamp / interval) * interval
|
||||
result = `FLOOR(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(typeof unit === 'string', 'unit should be string');
|
||||
console.warn('暂不支持 $dateFloor 对月年单位的处理');
|
||||
throw new Error(`Unsupported date floor unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
else if (fnArgs instanceof Array) {
|
||||
// 原有的数组参数处理逻辑
|
||||
const fnName = this.translateFnName(fnKey, fnArgs.length);
|
||||
const args = [fnName];
|
||||
args.push(...(expr)[k[0]].map((ele) => {
|
||||
args.push(...fnArgs.map((ele) => {
|
||||
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
|
||||
return translateConstant(ele);
|
||||
}
|
||||
|
|
@ -873,9 +1016,10 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
result = util_1.format.apply(null, args);
|
||||
}
|
||||
else {
|
||||
const fnName = this.translateFnName(k[0], 1);
|
||||
// 原有的单参数处理逻辑
|
||||
const fnName = this.translateFnName(fnKey, 1);
|
||||
const args = [fnName];
|
||||
const arg = (expr)[k[0]];
|
||||
const arg = fnArgs;
|
||||
if (['string', 'number'].includes(typeof arg) || arg instanceof Date) {
|
||||
args.push(translateConstant(arg));
|
||||
}
|
||||
|
|
@ -949,7 +1093,7 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
}
|
||||
// 新的remove应该包含$$deleteAt$$的值了
|
||||
/* const now = Date.now();
|
||||
|
||||
|
||||
const updateText2 = updateText ? `${updateText}, \`${alias}\`.\`$$deleteAt$$\` = '${now}'` : `\`${alias}\`.\`$$deleteAt$$\` = '${now}'`; */
|
||||
(0, assert_1.default)(updateText.includes(types_1.DeleteAtAttribute));
|
||||
let sql = `update ${fromText} set ${updateText}`;
|
||||
|
|
@ -965,5 +1109,99 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
}
|
||||
return sql;
|
||||
}
|
||||
/**
|
||||
* 将MySQL返回的Type回译成oak的类型,是 populateDataTypeDef 的反函数
|
||||
* @param type
|
||||
*/
|
||||
reTranslateToAttribute(type) {
|
||||
const withLengthDataTypes = MySqlTranslator.withLengthDataTypes.join('|');
|
||||
let result = (new RegExp(`^(${withLengthDataTypes})\\((\\d+)\\)$`)).exec(type);
|
||||
if (result) {
|
||||
return {
|
||||
type: result[1],
|
||||
params: {
|
||||
length: parseInt(result[2]),
|
||||
}
|
||||
};
|
||||
}
|
||||
const withPrecisionDataTypes = MySqlTranslator.withPrecisionDataTypes.join('|');
|
||||
result = (new RegExp(`^(${withPrecisionDataTypes})\\((\\d+),(d+)\\)$`)).exec(type);
|
||||
if (result) {
|
||||
return {
|
||||
type: result[1],
|
||||
params: {
|
||||
precision: parseInt(result[2]),
|
||||
scale: parseInt(result[3]),
|
||||
},
|
||||
};
|
||||
}
|
||||
result = (/^enum\((\S+)\)$/).exec(type);
|
||||
if (result) {
|
||||
const enumeration = result[1].split(',').map(ele => ele.slice(1, -1));
|
||||
return {
|
||||
type: 'enum',
|
||||
enumeration
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: type,
|
||||
};
|
||||
}
|
||||
// 分析当前数据库结构图
|
||||
async readSchema(execFn) {
|
||||
const result = {};
|
||||
const sql = 'show tables;';
|
||||
const [tables] = await execFn(sql);
|
||||
for (const tableItem of tables) {
|
||||
const table = Object.values(tableItem)[0];
|
||||
const [tableResult] = await execFn(`desc \`${table}\``);
|
||||
const attributes = {};
|
||||
for (const attrItem of tableResult) {
|
||||
const { Field: attrName, Null: isNull, Type: type, Key: key } = attrItem;
|
||||
attributes[attrName] = {
|
||||
...this.reTranslateToAttribute(type),
|
||||
notNull: isNull.toUpperCase() === 'NO',
|
||||
unique: key.toUpperCase() === 'UNI',
|
||||
};
|
||||
// 自增列只可能是seq
|
||||
if (attrName === '$$seq$$') {
|
||||
attributes[attrName].sequenceStart = 10000;
|
||||
}
|
||||
}
|
||||
Object.assign(result, {
|
||||
[table]: {
|
||||
attributes,
|
||||
}
|
||||
});
|
||||
const [indexedColumns] = (await execFn(`show index from \`${table}\``));
|
||||
if (indexedColumns.length) {
|
||||
const groupedColumns = (0, lodash_1.groupBy)(indexedColumns.sort((ele1, ele2) => ele1.Key_name.localeCompare(ele2.Key_name) || ele1.Seq_in_index - ele2.Seq_in_index), 'Key_name');
|
||||
const indexes = Object.values(groupedColumns).map((ele) => {
|
||||
const index = {
|
||||
name: ele[0].Key_name,
|
||||
attributes: ele.map(ele2 => ({
|
||||
name: ele2.Column_name,
|
||||
direction: ele2.Collation === 'D' ? 'DESC' : (ele2.Collation === 'A' ? 'ASC' : undefined),
|
||||
size: ele2.Sub_part || undefined,
|
||||
})),
|
||||
};
|
||||
if (ele[0].Non_unique === 0 || ele[0].Index_type.toUpperCase() !== 'BTREE') {
|
||||
index.config = {};
|
||||
if (ele[0].Non_unique === 0) {
|
||||
index.config.unique = true;
|
||||
}
|
||||
if (ele[0].Index_type.toUpperCase() !== 'BTREE') {
|
||||
index.config.type = ele[0].Index_type.toLowerCase();
|
||||
}
|
||||
}
|
||||
return index;
|
||||
});
|
||||
Object.assign(result[table], {
|
||||
indexes,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
exports.MySqlTranslator = MySqlTranslator;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg';
|
||||
import { TxnOption } from 'oak-domain/lib/types';
|
||||
import { PostgreSQLConfiguration } from './types/Configuration';
|
||||
export declare class PostgreSQLConnector {
|
||||
pool: Pool;
|
||||
configuration: PostgreSQLConfiguration;
|
||||
txnDict: Record<string, PoolClient>;
|
||||
constructor(configuration: PostgreSQLConfiguration);
|
||||
connect(): void;
|
||||
disconnect(): Promise<void>;
|
||||
startTransaction(option?: TxnOption): Promise<string>;
|
||||
/**
|
||||
* 映射隔离级别到 PostgreSQL 语法
|
||||
*/
|
||||
private mapIsolationLevel;
|
||||
exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]>;
|
||||
commitTransaction(txn: string): Promise<void>;
|
||||
rollbackTransaction(txn: string): Promise<void>;
|
||||
/**
|
||||
* 执行多条 SQL 语句(用于初始化等场景)
|
||||
*/
|
||||
execBatch(sqls: string[], txn?: string): Promise<void>;
|
||||
/**
|
||||
* 获取连接池状态
|
||||
*/
|
||||
getPoolStatus(): {
|
||||
total: number;
|
||||
idle: number;
|
||||
waiting: number;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PostgreSQLConnector = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const pg_1 = require("pg");
|
||||
const uuid_1 = require("uuid");
|
||||
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||||
class PostgreSQLConnector {
|
||||
pool;
|
||||
configuration;
|
||||
txnDict;
|
||||
constructor(configuration) {
|
||||
this.configuration = configuration;
|
||||
this.txnDict = {};
|
||||
this.pool = new pg_1.Pool(configuration);
|
||||
// 错误处理
|
||||
this.pool.on('error', (err) => {
|
||||
console.error('PostgreSQL pool error:', err);
|
||||
});
|
||||
}
|
||||
connect() {
|
||||
// pg Pool 是惰性连接,不需要显式连接
|
||||
// 但可以在这里进行连接测试
|
||||
}
|
||||
async disconnect() {
|
||||
// 先获取所有事务ID的快照
|
||||
const txnIds = Object.keys(this.txnDict);
|
||||
for (const txnId of txnIds) {
|
||||
try {
|
||||
await this.rollbackTransaction(txnId);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to rollback transaction ${txnId}:`, e);
|
||||
}
|
||||
}
|
||||
await this.pool.end();
|
||||
}
|
||||
async startTransaction(option) {
|
||||
const startInner = async () => {
|
||||
const connection = await this.pool.connect();
|
||||
// 添加:检测连接是否已被其他事务占用
|
||||
for (const txn2 in this.txnDict) {
|
||||
if (this.txnDict[txn2] === connection) {
|
||||
return new Promise((resolve) => {
|
||||
this.pool.on('release', () => resolve(startInner()));
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
let beginStmt = 'BEGIN';
|
||||
if (option?.isolationLevel) {
|
||||
// PostgreSQL 隔离级别:
|
||||
// READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
|
||||
// 注意: PostgreSQL 的 READ UNCOMMITTED 行为等同于 READ COMMITTED
|
||||
const level = this.mapIsolationLevel(option.isolationLevel);
|
||||
beginStmt = `BEGIN ISOLATION LEVEL ${level}`;
|
||||
}
|
||||
await connection.query(beginStmt);
|
||||
const id = (0, uuid_1.v4)();
|
||||
this.txnDict[id] = connection;
|
||||
return id;
|
||||
}
|
||||
catch (error) {
|
||||
// 如果启动事务失败,释放连接
|
||||
connection.release();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return startInner();
|
||||
}
|
||||
/**
|
||||
* 映射隔离级别到 PostgreSQL 语法
|
||||
*/
|
||||
mapIsolationLevel(level) {
|
||||
const levelUpper = level.toUpperCase().replace(/[-_]/g, ' '); // 同时处理 - 和 _
|
||||
const validLevels = [
|
||||
'READ UNCOMMITTED',
|
||||
'READ COMMITTED',
|
||||
'REPEATABLE READ',
|
||||
'SERIALIZABLE'
|
||||
];
|
||||
if (validLevels.includes(levelUpper)) {
|
||||
return levelUpper;
|
||||
}
|
||||
// 默认使用 READ COMMITTED
|
||||
console.warn(`Unknown isolation level "${level}", using READ COMMITTED`);
|
||||
return 'READ COMMITTED';
|
||||
}
|
||||
async exec(sql, txn) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('SQL:', sql);
|
||||
}
|
||||
try {
|
||||
let result;
|
||||
if (txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
(0, assert_1.default)(connection, `Transaction ${txn} not found`);
|
||||
result = await connection.query(sql);
|
||||
}
|
||||
else {
|
||||
result = await this.pool.query(sql);
|
||||
}
|
||||
// 返回格式与 mysql2 兼容: [rows, fields/result]
|
||||
return [result.rows, result];
|
||||
}
|
||||
catch (error) {
|
||||
// 增强错误信息
|
||||
const enhancedError = new Error(`PostgreSQL query failed: ${error.message}\nSQL: ${sql.slice(0, 500)}${sql.length > 500 ? '...' : ''}`);
|
||||
enhancedError.originalError = error;
|
||||
enhancedError.sql = sql;
|
||||
throw enhancedError;
|
||||
}
|
||||
}
|
||||
async commitTransaction(txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
(0, assert_1.default)(connection, `Transaction ${txn} not found`);
|
||||
try {
|
||||
await connection.query('COMMIT');
|
||||
}
|
||||
finally {
|
||||
delete this.txnDict[txn];
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
async rollbackTransaction(txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
(0, assert_1.default)(connection, `Transaction ${txn} not found`);
|
||||
try {
|
||||
await connection.query('ROLLBACK');
|
||||
}
|
||||
finally {
|
||||
delete this.txnDict[txn];
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 执行多条 SQL 语句(用于初始化等场景)
|
||||
*/
|
||||
async execBatch(sqls, txn) {
|
||||
for (const sql of sqls) {
|
||||
if (sql.trim()) {
|
||||
await this.exec(sql, txn);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取连接池状态
|
||||
*/
|
||||
getPoolStatus() {
|
||||
return {
|
||||
total: this.pool.totalCount,
|
||||
idle: this.pool.idleCount,
|
||||
waiting: this.pool.waitingCount
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.PostgreSQLConnector = PostgreSQLConnector;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult } from 'oak-domain/lib/types';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { PostgreSQLConfiguration } from './types/Configuration';
|
||||
import { PostgreSQLConnector } from './connector';
|
||||
import { PostgreSQLTranslator, PostgreSQLSelectOption, PostgreSQLOperateOption } from './translator';
|
||||
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { CreateEntityOption } from '../types/Translator';
|
||||
import { DbStore } from '../types/dbStore';
|
||||
export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
||||
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
|
||||
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
|
||||
protected selectAbjointRow<T extends keyof ED, OP extends SelectOption>(entity: T, selection: ED[T]['Selection'], context: SyncContext<ED>, option: OP): Partial<ED[T]['Schema']>[];
|
||||
protected updateAbjointRow<T extends keyof ED, OP extends OperateOption>(entity: T, operation: ED[T]['Operation'], context: SyncContext<ED>, option: OP): number;
|
||||
exec(script: string, txnId?: string): Promise<void>;
|
||||
connector: PostgreSQLConnector;
|
||||
translator: PostgreSQLTranslator<ED>;
|
||||
constructor(storageSchema: StorageSchema<ED>, configuration: PostgreSQLConfiguration);
|
||||
checkRelationAsync<T extends keyof ED, Cxt extends AsyncContext<ED>>(entity: T, operation: Omit<ED[T]['Operation'] | ED[T]['Selection'], 'id'>, context: Cxt): Promise<void>;
|
||||
protected aggregateAbjointRowAsync<T extends keyof ED, OP extends SelectOption, Cxt extends AsyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
|
||||
aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
|
||||
protected supportManyToOneJoin(): boolean;
|
||||
protected supportMultipleCreate(): boolean;
|
||||
private formResult;
|
||||
protected selectAbjointRowAsync<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: AsyncContext<ED>, option?: PostgreSQLSelectOption): Promise<Partial<ED[T]['Schema']>[]>;
|
||||
protected updateAbjointRowAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, option?: PostgreSQLOperateOption): Promise<number>;
|
||||
operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
|
||||
select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
|
||||
protected countAbjointRowAsync<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: AsyncContext<ED>, option: SelectOption): Promise<number>;
|
||||
count<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: SelectOption): Promise<number>;
|
||||
begin(option?: TxnOption): Promise<string>;
|
||||
commit(txnId: string): Promise<void>;
|
||||
rollback(txnId: string): Promise<void>;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
initialize(option: CreateEntityOption): Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PostgreSQLStore = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const CascadeStore_1 = require("oak-domain/lib/store/CascadeStore");
|
||||
const connector_1 = require("./connector");
|
||||
const translator_1 = require("./translator");
|
||||
const lodash_1 = require("lodash");
|
||||
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||||
const relation_1 = require("oak-domain/lib/store/relation");
|
||||
function convertGeoTextToObject(geoText) {
|
||||
if (geoText.startsWith('POINT')) {
|
||||
const coord = geoText.match(/(-?\d+\.?\d*)/g);
|
||||
(0, assert_1.default)(coord && coord.length === 2);
|
||||
return {
|
||||
type: 'point',
|
||||
coordinate: coord.map(ele => parseFloat(ele)),
|
||||
};
|
||||
}
|
||||
else if (geoText.startsWith('LINESTRING')) {
|
||||
const coordsMatch = geoText.match(/\(([^)]+)\)/);
|
||||
if (coordsMatch) {
|
||||
const points = coordsMatch[1].split(',').map(p => {
|
||||
const [x, y] = p.trim().split(/\s+/).map(parseFloat);
|
||||
return [x, y];
|
||||
});
|
||||
return {
|
||||
type: 'path',
|
||||
coordinate: points,
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (geoText.startsWith('POLYGON')) {
|
||||
const ringsMatch = geoText.match(/\(\(([^)]+)\)\)/g);
|
||||
if (ringsMatch) {
|
||||
const rings = ringsMatch.map(ring => {
|
||||
const coordStr = ring.replace(/[()]/g, '');
|
||||
return coordStr.split(',').map(p => {
|
||||
const [x, y] = p.trim().split(/\s+/).map(parseFloat);
|
||||
return [x, y];
|
||||
});
|
||||
});
|
||||
return {
|
||||
type: 'polygon',
|
||||
coordinate: rings,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported geometry type: ${geoText.slice(0, 50)}`);
|
||||
}
|
||||
class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
||||
countAbjointRow(entity, selection, context, option) {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
aggregateAbjointRowSync(entity, aggregation, context, option) {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
selectAbjointRow(entity, selection, context, option) {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
updateAbjointRow(entity, operation, context, option) {
|
||||
throw new Error('PostgreSQL store 不支持同步更新数据');
|
||||
}
|
||||
async exec(script, txnId) {
|
||||
await this.connector.exec(script, txnId);
|
||||
}
|
||||
connector;
|
||||
translator;
|
||||
constructor(storageSchema, configuration) {
|
||||
super(storageSchema);
|
||||
this.connector = new connector_1.PostgreSQLConnector(configuration);
|
||||
this.translator = new translator_1.PostgreSQLTranslator(storageSchema);
|
||||
}
|
||||
checkRelationAsync(entity, operation, context) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async aggregateAbjointRowAsync(entity, aggregation, context, option) {
|
||||
const sql = this.translator.translateAggregate(entity, aggregation, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
return this.formResult(entity, result[0]);
|
||||
}
|
||||
aggregate(entity, aggregation, context, option) {
|
||||
return this.aggregateAsync(entity, aggregation, context, option);
|
||||
}
|
||||
supportManyToOneJoin() {
|
||||
return true;
|
||||
}
|
||||
supportMultipleCreate() {
|
||||
return true;
|
||||
}
|
||||
formResult(entity, result) {
|
||||
const schema = this.getSchema();
|
||||
function resolveAttribute(entity2, r, attr, value) {
|
||||
const { attributes, view } = schema[entity2];
|
||||
if (!view) {
|
||||
const i = attr.indexOf(".");
|
||||
if (i !== -1) {
|
||||
const attrHead = attr.slice(0, i);
|
||||
const attrTail = attr.slice(i + 1);
|
||||
const rel = (0, relation_1.judgeRelation)(schema, entity2, attrHead);
|
||||
if (rel === 1) {
|
||||
(0, lodash_1.set)(r, attr, value);
|
||||
}
|
||||
else {
|
||||
if (!r[attrHead]) {
|
||||
r[attrHead] = {};
|
||||
}
|
||||
if (rel === 0) {
|
||||
resolveAttribute(entity2, r[attrHead], attrTail, value);
|
||||
}
|
||||
else if (rel === 2) {
|
||||
resolveAttribute(attrHead, r[attrHead], attrTail, value);
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(typeof rel === 'string');
|
||||
resolveAttribute(rel, r[attrHead], attrTail, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (attributes[attr]) {
|
||||
const { type } = attributes[attr];
|
||||
switch (type) {
|
||||
case 'date':
|
||||
case 'time': {
|
||||
if (value instanceof Date) {
|
||||
r[attr] = value.valueOf();
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'geometry': {
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = convertGeoTextToObject(value);
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'object':
|
||||
case 'array': {
|
||||
// PostgreSQL jsonb 直接返回对象,不需要 parse
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = JSON.parse(value);
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = `return ${Buffer.from(value, 'base64').toString()}`;
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'bool':
|
||||
case 'boolean': {
|
||||
// PostgreSQL 直接返回 boolean 类型
|
||||
r[attr] = value;
|
||||
break;
|
||||
}
|
||||
case 'decimal': {
|
||||
// PostgreSQL numeric 类型可能返回字符串
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = parseFloat(value);
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(value === null || typeof value === 'number');
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: 这里和mysql统一行为,ref类型的字符串去除前后空格
|
||||
case "char":
|
||||
case "ref": {
|
||||
if (value) {
|
||||
(0, assert_1.default)(typeof value === 'string');
|
||||
r[attr] = value.trim();
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
r[attr] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO: 这里和mysql统一行为,id字段为char类型时,去除后面的空格
|
||||
if (value && typeof value === 'string') {
|
||||
if (attr === 'id') {
|
||||
r[attr] = value.trim();
|
||||
}
|
||||
else if (attr.startsWith("#count")) {
|
||||
// PostgreSQL count 返回字符串
|
||||
r[attr] = parseInt(value, 10);
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
r[attr] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
(0, lodash_1.assign)(r, { [attr]: value });
|
||||
}
|
||||
}
|
||||
function removeNullObjects(r, e) {
|
||||
for (let attr in r) {
|
||||
const rel = (0, relation_1.judgeRelation)(schema, e, attr);
|
||||
if (rel === 2) {
|
||||
if (r[attr].id === null) {
|
||||
(0, assert_1.default)(schema[e].toModi || r.entity !== attr);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
removeNullObjects(r[attr], attr);
|
||||
}
|
||||
else if (typeof rel === 'string') {
|
||||
if (r[attr].id === null) {
|
||||
(0, assert_1.default)(schema[e].toModi || r[`${attr}Id`] === null, `对象${String(e)}取数据时,发现其外键找不到目标对象,rowId是${r.id},其外键${attr}Id值为${r[`${attr}Id`]}`);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
removeNullObjects(r[attr], rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
function formSingleRow(r) {
|
||||
let result2 = {};
|
||||
for (let attr in r) {
|
||||
const value = r[attr];
|
||||
resolveAttribute(entity, result2, attr, value);
|
||||
}
|
||||
removeNullObjects(result2, entity);
|
||||
return result2;
|
||||
}
|
||||
if (result instanceof Array) {
|
||||
return result.map(r => formSingleRow(r));
|
||||
}
|
||||
return formSingleRow(result);
|
||||
}
|
||||
async selectAbjointRowAsync(entity, selection, context, option) {
|
||||
const sql = this.translator.translateSelect(entity, selection, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
return this.formResult(entity, result[0]);
|
||||
}
|
||||
async updateAbjointRowAsync(entity, operation, context, option) {
|
||||
const { translator, connector } = this;
|
||||
const { action } = operation;
|
||||
const txn = context.getCurrentTxnId();
|
||||
switch (action) {
|
||||
case 'create': {
|
||||
const { data } = operation;
|
||||
const sql = translator.translateInsert(entity, data instanceof Array ? data : [data]);
|
||||
const result = await connector.exec(sql, txn);
|
||||
// PostgreSQL QueryResult.rowCount
|
||||
return result[1].rowCount || 0;
|
||||
}
|
||||
case 'remove': {
|
||||
const sql = translator.translateRemove(entity, operation, option);
|
||||
const result = await connector.exec(sql, txn);
|
||||
return result[1].rowCount || 0;
|
||||
}
|
||||
default: {
|
||||
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action));
|
||||
const sql = translator.translateUpdate(entity, operation, option);
|
||||
const result = await connector.exec(sql, txn);
|
||||
return result[1].rowCount || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
async operate(entity, operation, context, option) {
|
||||
const { action } = operation;
|
||||
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '不支持使用 select operation');
|
||||
return await super.operateAsync(entity, operation, context, option);
|
||||
}
|
||||
async select(entity, selection, context, option) {
|
||||
return await super.selectAsync(entity, selection, context, option);
|
||||
}
|
||||
async countAbjointRowAsync(entity, selection, context, option) {
|
||||
const sql = this.translator.translateCount(entity, selection, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
// PostgreSQL 返回的 count 是 string 类型(bigint)
|
||||
const cnt = result[0][0]?.cnt;
|
||||
return typeof cnt === 'string' ? parseInt(cnt, 10) : (cnt || 0);
|
||||
}
|
||||
async count(entity, selection, context, option) {
|
||||
return this.countAsync(entity, selection, context, option);
|
||||
}
|
||||
async begin(option) {
|
||||
return await this.connector.startTransaction(option);
|
||||
}
|
||||
async commit(txnId) {
|
||||
await this.connector.commitTransaction(txnId);
|
||||
}
|
||||
async rollback(txnId) {
|
||||
await this.connector.rollbackTransaction(txnId);
|
||||
}
|
||||
async connect() {
|
||||
await this.connector.connect();
|
||||
}
|
||||
async disconnect() {
|
||||
await this.connector.disconnect();
|
||||
}
|
||||
async initialize(option) {
|
||||
const schema = this.getSchema();
|
||||
// 可选:先创建 PostGIS 扩展
|
||||
// await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
|
||||
for (const entity in schema) {
|
||||
const sqls = this.translator.translateCreateEntity(entity, option);
|
||||
for (const sql of sqls) {
|
||||
await this.connector.exec(sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.PostgreSQLStore = PostgreSQLStore;
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { EntityDict, Q_FullTextValue, RefOrExpression, Ref, StorageSchema } from "oak-domain/lib/types";
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
|
||||
import { SqlOperateOption, SqlSelectOption, SqlTranslator } from "../sqlTranslator";
|
||||
import { CreateEntityOption } from '../types/Translator';
|
||||
export interface PostgreSQLSelectOption extends SqlSelectOption {
|
||||
}
|
||||
export interface PostgreSQLOperateOption extends SqlOperateOption {
|
||||
}
|
||||
export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extends SqlTranslator<ED> {
|
||||
private getEnumTypeName;
|
||||
/**
|
||||
* 将 MySQL 风格的 JSON 路径转换为 PostgreSQL 路径数组格式
|
||||
* 例如: ".foo.bar[0].baz" -> '{foo,bar,0,baz}'
|
||||
*/
|
||||
private convertJsonPath;
|
||||
/**
|
||||
* 生成 PostgreSQL JSON 访问表达式
|
||||
* @param column 列名(包含别名)
|
||||
* @param path JSON 路径
|
||||
* @param asText 是否返回文本(使用 #>> 而不是 #>)
|
||||
*/
|
||||
private buildJsonAccessor;
|
||||
protected getDefaultSelectFilter(alias: string, option?: PostgreSQLSelectOption): string;
|
||||
private makeUpSchema;
|
||||
constructor(schema: StorageSchema<ED>);
|
||||
static supportedDataTypes: DataType[];
|
||||
static spatialTypes: DataType[];
|
||||
static withLengthDataTypes: DataType[];
|
||||
static withPrecisionDataTypes: DataType[];
|
||||
static withScaleDataTypes: DataType[];
|
||||
static dataTypeDefaults: Record<string, any>;
|
||||
maxAliasLength: number;
|
||||
private populateDataTypeDef;
|
||||
/**
|
||||
* PostgreSQL 字符串值转义
|
||||
* 防御SQL注入
|
||||
*/
|
||||
escapeStringValue(value: string): string;
|
||||
/**
|
||||
* LIKE 模式转义
|
||||
* 转义 LIKE 语句中的特殊字符
|
||||
*/
|
||||
escapeLikePattern(value: string): string;
|
||||
/**
|
||||
* PostgreSQL 标识符转义
|
||||
* 用于表名、列名等
|
||||
*/
|
||||
escapeIdentifier(identifier: string): string;
|
||||
/**
|
||||
* tsquery 搜索词转义
|
||||
*/
|
||||
escapeTsQueryValue(value: string): string;
|
||||
quoteIdentifier(identifier: string): string;
|
||||
protected translateAttrProjection(dataType: DataType, alias: string, attr: string): string;
|
||||
protected translateObjectPredicate(predicate: Record<string, any>, alias: string, attr: string): string;
|
||||
protected translateObjectProjection(projection: Record<string, any>, alias: string, attr: string, prefix: string): string;
|
||||
protected translateAttrValue(dataType: DataType | Ref, value: any): string;
|
||||
protected translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string;
|
||||
translateCreateEntity<T extends keyof ED>(entity: T, options?: CreateEntityOption): string[];
|
||||
private translateFnName;
|
||||
private translateAttrInExpression;
|
||||
protected translateExpression<T extends keyof ED>(entity: T, alias: string, expression: RefOrExpression<keyof ED[T]["OpSchema"]>, refDict: Record<string, [string, keyof ED]>): string;
|
||||
protected populateSelectStmt<T extends keyof ED>(projectionText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, groupByText?: string, indexFrom?: number, count?: number, option?: PostgreSQLSelectOption): string;
|
||||
translateUpdate<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Update'], option?: OP): string;
|
||||
translateRemove<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Remove'], option?: OP): string;
|
||||
/**
|
||||
* PostgreSQL专用的结构化JOIN分析
|
||||
* 返回结构化的JOIN信息,而不是拼接好的FROM字符串
|
||||
*/
|
||||
private analyzeJoinStructured;
|
||||
/**
|
||||
* 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
|
||||
*/
|
||||
private buildJoinConditions;
|
||||
/**
|
||||
* 生成 PostgreSQL UPSERT 语句
|
||||
* INSERT ... ON CONFLICT (key) DO UPDATE SET ...
|
||||
*/
|
||||
translateUpsert<T extends keyof ED>(entity: T, data: ED[T]['CreateMulti']['data'], conflictKeys: string[], updateAttrs?: string[]): string;
|
||||
protected populateUpdateStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
|
||||
protected populateRemoveStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: PostgreSQLOperateOption): string;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
export type PostgreSQLConfiguration = {
|
||||
host: string;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
port?: number;
|
||||
max?: number;
|
||||
idleTimeoutMillis?: number;
|
||||
connectionTimeoutMillis?: number;
|
||||
};
|
||||
export type Configuration = {
|
||||
postgresql: PostgreSQLConfiguration;
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export * from './MySQL/store';
|
||||
export { MySqlSelectOption, MysqlOperateOption } from './MySQL/translator';
|
||||
export * from './PostgreSQL/store';
|
||||
export { PostgreSQLSelectOption, PostgreSQLOperateOption } from './PostgreSQL/translator';
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const tslib_1 = require("tslib");
|
||||
tslib_1.__exportStar(require("./MySQL/store"), exports);
|
||||
tslib_1.__exportStar(require("./PostgreSQL/store"), exports);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export declare abstract class SqlTranslator<ED extends EntityDict & BaseEntityDi
|
|||
protected abstract populateUpdateStmt<OP extends SqlOperateOption>(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: OP): string;
|
||||
protected abstract populateRemoveStmt<OP extends SqlOperateOption>(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, option?: OP): string;
|
||||
protected abstract translateExpression<T extends keyof ED>(entity: T, alias: string, expression: RefOrExpression<keyof ED[T]['OpSchema']>, refDict: Record<string, [string, keyof ED]>): string;
|
||||
private getStorageName;
|
||||
protected getStorageName<T extends keyof ED>(entity: T): string;
|
||||
translateInsert<T extends keyof ED>(entity: T, data: ED[T]['CreateMulti']['data']): string;
|
||||
/**
|
||||
* analyze the join relations in projection/query/sort
|
||||
|
|
@ -37,10 +37,20 @@ export declare abstract class SqlTranslator<ED extends EntityDict & BaseEntityDi
|
|||
* @param param0
|
||||
*/
|
||||
private analyzeJoin;
|
||||
/**
|
||||
* 对like模式中的特殊字符进行转义
|
||||
* 例如 % 和 _,以防止被当成通配符处理
|
||||
* @param pattern like模式字符串
|
||||
* @returns 转义后的字符串
|
||||
*/
|
||||
escapeLikePattern(pattern: string): string;
|
||||
private translateComparison;
|
||||
private translateEvaluation;
|
||||
protected translatePredicate(predicate: string, value: any, type?: DataType | Ref): string;
|
||||
private translateFilter;
|
||||
protected translateFilter<T extends keyof ED, OP extends SqlSelectOption>(entity: T, filter: ED[T]['Selection']['filter'], aliasDict: Record<string, string>, filterRefAlias: Record<string, [string, keyof ED]>, initialNumber: number, option?: OP): {
|
||||
stmt: string;
|
||||
currentNumber: number;
|
||||
};
|
||||
private translateSorter;
|
||||
private translateProjection;
|
||||
private translateSelectInner;
|
||||
|
|
@ -52,4 +62,7 @@ export declare abstract class SqlTranslator<ED extends EntityDict & BaseEntityDi
|
|||
translateUpdate<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Update'], option?: OP): string;
|
||||
translateDestroyEntity(entity: string, truncate?: boolean): string;
|
||||
escapeStringValue(value: string): string;
|
||||
/**比较两段sql是否完全一致,这里是把所有的空格去掉了 */
|
||||
compareSql(sql1: string, sql2: string): boolean;
|
||||
quoteIdentifier(name: string): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class SqlTranslator {
|
|||
const { attributes, indexes } = schema[entity];
|
||||
// 增加默认的属性
|
||||
(0, lodash_1.assign)(attributes, {
|
||||
id: {
|
||||
[types_1.PrimaryKeyAttribute]: {
|
||||
type: 'char',
|
||||
params: {
|
||||
length: 36,
|
||||
|
|
@ -69,7 +69,7 @@ class SqlTranslator {
|
|||
name: types_1.DeleteAtAttribute,
|
||||
}],
|
||||
}, {
|
||||
name: `${entity}_trigger_uuid`,
|
||||
name: `${entity}_trigger_uuid_auto_create`,
|
||||
attributes: [{
|
||||
name: types_1.TriggerUuidAttribute,
|
||||
}]
|
||||
|
|
@ -137,6 +137,16 @@ class SqlTranslator {
|
|||
}
|
||||
}
|
||||
if (indexes) {
|
||||
for (const index of indexes) {
|
||||
const { attributes, config } = index;
|
||||
if (!config?.type || config.type === 'btree') {
|
||||
if (!attributes.find((ele) => ele.name === types_1.DeleteAtAttribute)) {
|
||||
attributes.push({
|
||||
name: types_1.DeleteAtAttribute,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
indexes.push(...intrinsticIndexes);
|
||||
}
|
||||
else {
|
||||
|
|
@ -154,14 +164,14 @@ class SqlTranslator {
|
|||
translateInsert(entity, data) {
|
||||
const { schema } = this;
|
||||
const { attributes, storageName = entity } = schema[entity];
|
||||
let sql = `insert into \`${storageName}\`(`;
|
||||
let sql = `insert into ${this.quoteIdentifier(storageName)}(`;
|
||||
/**
|
||||
* 这里的attrs要用所有行的union集合
|
||||
*/
|
||||
const dataFull = data.reduce((prev, cur) => Object.assign({}, cur, prev), {});
|
||||
const attrs = Object.keys(dataFull).filter(ele => attributes.hasOwnProperty(ele));
|
||||
attrs.forEach((attr, idx) => {
|
||||
sql += ` \`${attr}\``;
|
||||
sql += ` ${this.quoteIdentifier(attr)}`;
|
||||
if (idx < attrs.length - 1) {
|
||||
sql += ',';
|
||||
}
|
||||
|
|
@ -209,7 +219,7 @@ class SqlTranslator {
|
|||
const projectionRefAlias = {};
|
||||
const filterRefAlias = {};
|
||||
const alias = `${entity}_${number++}`;
|
||||
let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
|
||||
let from = ` ${this.quoteIdentifier(this.getStorageName(entity))} ${this.quoteIdentifier(alias)} `;
|
||||
const aliasDict = {
|
||||
'./': alias,
|
||||
};
|
||||
|
|
@ -243,7 +253,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${op}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(op + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -263,7 +273,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(op)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${op}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(op))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${op}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -307,7 +317,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -327,7 +337,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${attr}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(attr))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${attr}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -365,7 +375,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -385,7 +395,7 @@ class SqlTranslator {
|
|||
(0, lodash_1.assign)(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${attr}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(attr))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${attr}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -426,6 +436,15 @@ class SqlTranslator {
|
|||
currentNumber: number,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 对like模式中的特殊字符进行转义
|
||||
* 例如 % 和 _,以防止被当成通配符处理
|
||||
* @param pattern like模式字符串
|
||||
* @returns 转义后的字符串
|
||||
*/
|
||||
escapeLikePattern(pattern) {
|
||||
return pattern.replace(/[%_]/g, (match) => `\\${match}`);
|
||||
}
|
||||
translateComparison(attr, value, type) {
|
||||
const SQL_OP = {
|
||||
$gt: '>',
|
||||
|
|
@ -445,16 +464,19 @@ class SqlTranslator {
|
|||
}
|
||||
switch (attr) {
|
||||
case '$startsWith': {
|
||||
return ` like '${value}%'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '${escaped}%'`;
|
||||
}
|
||||
case '$endsWith': {
|
||||
return ` like '%${value}'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '%${escaped}'`;
|
||||
}
|
||||
case '$includes': {
|
||||
return ` like '%${value}%'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '%${escaped}%'`;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unrecoganized comparison operator ${attr}`);
|
||||
throw new Error(`unrecognized comparison operator ${attr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -499,7 +521,7 @@ class SqlTranslator {
|
|||
};
|
||||
const values = value.map((v) => {
|
||||
if (type && ['varchar', 'char', 'text', 'nvarchar', 'ref', 'enum'].includes(type) || typeof v === 'string') {
|
||||
return `'${v}'`;
|
||||
return this.escapeStringValue(String(v));
|
||||
}
|
||||
else {
|
||||
return `${v}`;
|
||||
|
|
@ -516,7 +538,7 @@ class SqlTranslator {
|
|||
else if (predicate === '$between') {
|
||||
const values = value.map((v) => {
|
||||
if (type && ['varchar', 'char', 'text', 'nvarchar', 'ref', 'enum'].includes(type) || typeof v === 'string') {
|
||||
return `'${v}'`;
|
||||
return this.escapeStringValue(String(v));
|
||||
}
|
||||
else {
|
||||
return `${v}`;
|
||||
|
|
@ -626,7 +648,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 {
|
||||
/**
|
||||
|
|
@ -682,15 +704,18 @@ class SqlTranslator {
|
|||
whereText += `(${this.translateObjectPredicate(filter2[attr], alias, attr)})`;
|
||||
}
|
||||
else {
|
||||
if (!filter2[attr]) {
|
||||
throw new Error(`属性${attr}的查询条件不能为null或undefined`);
|
||||
}
|
||||
(0, assert_1.default)(Object.keys(filter2[attr]).length === 1);
|
||||
const predicate = Object.keys(filter2[attr])[0];
|
||||
(0, assert_1.default)(predicate.startsWith('$'));
|
||||
// 对属性上的谓词处理
|
||||
whereText += ` (\`${alias}\`.\`${attr}\` ${this.translatePredicate(predicate, filter2[attr][predicate], type2)})`;
|
||||
whereText += ` (${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} ${this.translatePredicate(predicate, filter2[attr][predicate], type2)})`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
whereText += ` (\`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])})`;
|
||||
whereText += ` (${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${this.translateAttrValue(type2, filter2[attr])})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -716,7 +741,7 @@ class SqlTranslator {
|
|||
return this.translateExpression(entity2, alias, sortAttr[attr], {});
|
||||
}
|
||||
else if (sortAttr[attr] === 1) {
|
||||
return `\`${alias}\`.\`${attr}\``;
|
||||
return `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)}`;
|
||||
}
|
||||
else {
|
||||
const rel = (0, relation_1.judgeRelation)(this.schema, entity2, attr);
|
||||
|
|
@ -765,12 +790,12 @@ class SqlTranslator {
|
|||
projText += ` ${exprText}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${exprText} as \`${prefix2}${attr}\``;
|
||||
projText += ` ${exprText} as ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${attr}\``;
|
||||
as = this.quoteIdentifier(prefix2 + attr);
|
||||
}
|
||||
else {
|
||||
as += `, \`${prefix2}${attr}\``;
|
||||
as += `, ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -789,12 +814,12 @@ class SqlTranslator {
|
|||
projText += ` ${this.translateAttrProjection(type, alias, attr)}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${this.translateAttrProjection(type, alias, attr)} as \`${prefix2}${attr}\``;
|
||||
projText += ` ${this.translateAttrProjection(type, alias, attr)} as ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${attr}\``;
|
||||
as = this.quoteIdentifier(prefix2 + attr);
|
||||
}
|
||||
else {
|
||||
as += `, \`${prefix2}${attr}\``;
|
||||
as += `, ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -810,12 +835,12 @@ class SqlTranslator {
|
|||
projText += ` ${this.translateAttrProjection(type, alias, attr)}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${this.translateAttrProjection(type, alias, attr)} as \`${prefix2}${projection2[attr]}\``;
|
||||
projText += ` ${this.translateAttrProjection(type, alias, attr)} as ${this.quoteIdentifier(`${prefix2}${projection2[attr]}`)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${projection2[attr]}\``;
|
||||
as = this.quoteIdentifier(`${prefix2}${projection2[attr]}`);
|
||||
}
|
||||
else {
|
||||
as += `\`${prefix2}${projection2[attr]}\``;
|
||||
as += `, ${this.quoteIdentifier(`${prefix2}${projection2[attr]}`)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -885,29 +910,29 @@ class SqlTranslator {
|
|||
let { projText: projSubText } = this.translateProjection(entity, data[k], aliasDict, projectionRefAlias, undefined, true);
|
||||
let projSubText2 = '';
|
||||
if (k.startsWith('#max')) {
|
||||
projSubText2 = `max(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `max(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#min')) {
|
||||
projSubText2 = `min(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `min(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#count')) {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
projSubText2 = `count(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `count(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#sum')) {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
projSubText2 = `sum(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `sum(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
(0, assert_1.default)(k.startsWith('#avg'));
|
||||
projSubText2 = `avg(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `avg(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
if (!projText) {
|
||||
projText = projSubText2;
|
||||
|
|
@ -955,7 +980,7 @@ class SqlTranslator {
|
|||
// delete只支持对volatile trigger的metadata域赋值
|
||||
(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]);
|
||||
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
|
||||
updateText += `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${value}`;
|
||||
}
|
||||
}
|
||||
return this.populateRemoveStmt(updateText, fromText, aliasDict, filterText, /* sorterText */ undefined, indexFrom, count, option);
|
||||
|
|
@ -973,7 +998,7 @@ class SqlTranslator {
|
|||
}
|
||||
(0, assert_1.default)(attributes.hasOwnProperty(attr));
|
||||
const value = this.translateAttrValue(attributes[attr].type, data[attr]);
|
||||
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
|
||||
updateText += `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${value}`;
|
||||
}
|
||||
const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, option);
|
||||
// const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
|
||||
|
|
@ -984,10 +1009,10 @@ class SqlTranslator {
|
|||
const { storageName = entity, view } = schema[entity];
|
||||
let sql;
|
||||
if (view) {
|
||||
sql = `drop view if exists \`${storageName}\``;
|
||||
sql = `drop view if exists ${this.quoteIdentifier(storageName)}`;
|
||||
}
|
||||
else {
|
||||
sql = truncate ? `truncate table \`${storageName}\`` : `drop table if exists \`${storageName}\``;
|
||||
sql = truncate ? `truncate table ${this.quoteIdentifier(storageName)}` : `drop table if exists ${this.quoteIdentifier(storageName)}`;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
|
@ -995,5 +1020,13 @@ class SqlTranslator {
|
|||
const result = sqlstring_1.default.escape(value);
|
||||
return result;
|
||||
}
|
||||
/**比较两段sql是否完全一致,这里是把所有的空格去掉了 */
|
||||
compareSql(sql1, sql2) {
|
||||
const reg = /[\t\r\f\n\s]/g;
|
||||
return sql1.replaceAll(reg, '') === sql2.replaceAll(reg, '');
|
||||
}
|
||||
quoteIdentifier(name) {
|
||||
return `\`${name}\``; // MySQL 默认
|
||||
}
|
||||
}
|
||||
exports.SqlTranslator = SqlTranslator;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { PostgreSQLConfiguration } from './../PostgreSQL/types/Configuration';
|
||||
import { MySQLConfiguration } from './../MySQL/types/Configuration';
|
||||
export type DbConfiguration = PostgreSQLConfiguration & {
|
||||
type: 'postgresql';
|
||||
} | MySQLConfiguration & {
|
||||
type: 'mysql';
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { EntityDict } from "oak-domain/lib/base-app-domain";
|
||||
import { AsyncContext, AsyncRowStore } from "oak-domain/lib/store/AsyncRowStore";
|
||||
import { CreateEntityOption } from "./Translator";
|
||||
export interface DbStore<ED extends EntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize(options: CreateEntityOption): Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
;
|
||||
13
package.json
13
package.json
|
|
@ -12,13 +12,18 @@
|
|||
"scripts": {
|
||||
"test": "mocha",
|
||||
"make:test:domain": "ts-node script/makeTestDomain.ts",
|
||||
"build": "tsc"
|
||||
"build": "tsc",
|
||||
"build:test": "tsc -p tsconfig.test.json",
|
||||
"test:mysql": "node ./test-dist/testMySQLStore.js",
|
||||
"test:postgres": "node ./test-dist/testPostgresStore.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"oak-domain": "../oak-domain",
|
||||
"oak-domain": "file:../oak-domain",
|
||||
"oak-general-business": "file:../oak-general-business",
|
||||
"pg": "^8.16.3",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"license": "ISC",
|
||||
|
|
@ -27,13 +32,13 @@
|
|||
"@types/luxon": "^2.3.2",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "^20.6.0",
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"mocha": "^10.2.0",
|
||||
"oak-general-business": "~5.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore'
|
|||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { CreateEntityOption } from '../types/Translator';
|
||||
import { FieldPacket, ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||
import { DbStore } from '../types/dbStore';
|
||||
|
||||
|
||||
function convertGeoTextToObject(geoText: string): Geo {
|
||||
|
|
@ -30,7 +31,7 @@ function convertGeoTextToObject(geoText: string): Geo {
|
|||
}
|
||||
}
|
||||
|
||||
export class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements AsyncRowStore<ED, Cxt> {
|
||||
export class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt>{
|
||||
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number {
|
||||
throw new Error('MySQL store不支持同步取数据,不应该跑到这儿');
|
||||
}
|
||||
|
|
@ -222,7 +223,7 @@ export class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends Asyn
|
|||
// 边界,如果是toModi的对象,这里的外键确实有可能为空
|
||||
// assert(schema[e].toModi || r[`${attr}Id`] === r[attr].id, `对象${<string>e}取数据时,发现其外键与连接的对象的主键不一致,rowId是${r.id},其${attr}Id值为${r[`${attr}Id`]},连接的对象的主键为${r[attr].id}`);
|
||||
if (r[attr].id === null) {
|
||||
assert(schema[e].toModi || r[`${attr}Id`] === null);
|
||||
assert(schema[e].toModi || r[`${attr}Id`] === null, `对象${String(e)}取数据时,发现其外键找不到目标对象,rowId是${r.id},其外键${attr}Id值为${r[`${attr}Id`]}`);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
|
|
@ -320,8 +321,8 @@ export class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends Asyn
|
|||
async rollback(txnId: string): Promise<void> {
|
||||
await this.connector.rollbackTransaction(txnId);
|
||||
}
|
||||
connect() {
|
||||
this.connector.connect();
|
||||
async connect() {
|
||||
await this.connector.connect();
|
||||
}
|
||||
async disconnect() {
|
||||
await this.connector.disconnect();
|
||||
|
|
|
|||
|
|
@ -889,10 +889,13 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
case '$dayOfYear': {
|
||||
return 'DAYOFYEAR(%s)';
|
||||
}
|
||||
case '$dateDiff': {
|
||||
assert(argumentNumber === 3);
|
||||
return 'DATEDIFF(%s, %s, %s)';
|
||||
}
|
||||
|
||||
// 这个实现有问题,DATEDIFF只是计算两个日期之间的天数差,只接受两个参数,放在translateExperession里实现
|
||||
// case '$dateDiff': {
|
||||
// assert(argumentNumber === 3);
|
||||
// return 'DATEDIFF(%s, %s, %s)';
|
||||
// }
|
||||
|
||||
case '$contains': {
|
||||
assert(argumentNumber === 2);
|
||||
return 'ST_CONTAINS(%s, %s)';
|
||||
|
|
@ -909,6 +912,24 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
result += ')';
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== 聚合函数 ==========
|
||||
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(`unrecoganized function ${fnName}`);
|
||||
}
|
||||
|
|
@ -955,10 +976,136 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
}
|
||||
else {
|
||||
assert(k.length === 1);
|
||||
if ((expr)[k[0]] instanceof Array) {
|
||||
const fnName = this.translateFnName(k[0], (expr)[k[0]].length);
|
||||
const fnKey = k[0];
|
||||
const fnArgs = (expr)[fnKey];
|
||||
|
||||
// 特殊处理日期相关函数
|
||||
if (fnKey === '$dateDiff') {
|
||||
// $dateDiff: [date1, date2, unit]
|
||||
assert(fnArgs instanceof Array && fnArgs.length === 3);
|
||||
const [date1Expr, date2Expr, unit] = fnArgs;
|
||||
|
||||
// 转换日期表达式
|
||||
const translateDateArg = (arg: any): string => {
|
||||
if (arg instanceof Date) {
|
||||
return `FROM_UNIXTIME(${arg.valueOf()} / 1000)`;
|
||||
} else if (typeof arg === 'number') {
|
||||
return `FROM_UNIXTIME(${arg} / 1000)`;
|
||||
} else {
|
||||
return translateInner(arg);
|
||||
}
|
||||
};
|
||||
|
||||
const date1Str = translateDateArg(date1Expr);
|
||||
const date2Str = translateDateArg(date2Expr);
|
||||
|
||||
// MySQL TIMESTAMPDIFF 单位映射
|
||||
const unitMap: Record<string, string> = {
|
||||
's': 'SECOND',
|
||||
'm': 'MINUTE',
|
||||
'h': 'HOUR',
|
||||
'd': 'DAY',
|
||||
'M': 'MONTH',
|
||||
'y': 'YEAR'
|
||||
};
|
||||
|
||||
const mysqlUnit = unitMap[unit];
|
||||
if (!mysqlUnit) {
|
||||
throw new Error(`Unsupported date diff unit: ${unit}`);
|
||||
}
|
||||
|
||||
// TIMESTAMPDIFF(unit, date1, date2) 返回 date2 - date1
|
||||
// 但类型定义是 date1 - date2,所以参数顺序要反过来
|
||||
result = `TIMESTAMPDIFF(${mysqlUnit}, ${date2Str}, ${date1Str})`;
|
||||
} else if (fnKey === '$dateCeil') {
|
||||
// $dateCeil: [date, unit]
|
||||
assert(fnArgs instanceof Array && fnArgs.length === 2);
|
||||
const [dateExpr, unit] = fnArgs;
|
||||
|
||||
const getTimestampExpr = (arg: any): string => {
|
||||
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: Record<string, number> = {
|
||||
's': 1000,
|
||||
'm': 60000,
|
||||
'h': 3600000,
|
||||
'd': 86400000,
|
||||
};
|
||||
|
||||
if (msPerUnit[unit]) {
|
||||
// CEIL 向上取整
|
||||
result = `CEIL(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||||
} else {
|
||||
assert(typeof unit === 'string', 'unit should be string');
|
||||
console.warn('暂不支持 $dateCeil 对月年单位的处理');
|
||||
throw new Error(`Unsupported date ceil unit: ${unit}`);
|
||||
}
|
||||
} else if (fnKey === '$dateFloor') {
|
||||
// $dateFloor: [date, unit]
|
||||
assert(fnArgs instanceof Array && fnArgs.length === 2);
|
||||
const [dateExpr, unit] = fnArgs;
|
||||
|
||||
// 获取毫秒时间戳表达式
|
||||
const getTimestampExpr = (arg: any): string => {
|
||||
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: Record<string, number> = {
|
||||
's': 1000,
|
||||
'm': 60000,
|
||||
'h': 3600000,
|
||||
'd': 86400000,
|
||||
};
|
||||
|
||||
if (msPerUnit[unit]) {
|
||||
// FLOOR(timestamp / interval) * interval
|
||||
result = `FLOOR(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
|
||||
} else {
|
||||
assert(typeof unit === 'string', 'unit should be string');
|
||||
console.warn('暂不支持 $dateFloor 对月年单位的处理');
|
||||
throw new Error(`Unsupported date floor unit: ${unit}`);
|
||||
}
|
||||
} else if (fnArgs instanceof Array) {
|
||||
// 原有的数组参数处理逻辑
|
||||
const fnName = this.translateFnName(fnKey, fnArgs.length);
|
||||
const args = [fnName];
|
||||
args.push(...(expr)[k[0]].map(
|
||||
args.push(...fnArgs.map(
|
||||
(ele: any) => {
|
||||
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
|
||||
return translateConstant(ele);
|
||||
|
|
@ -972,9 +1119,10 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
result = format.apply(null, args);
|
||||
}
|
||||
else {
|
||||
const fnName = this.translateFnName(k[0], 1);
|
||||
// 原有的单参数处理逻辑
|
||||
const fnName = this.translateFnName(fnKey, 1);
|
||||
const args = [fnName];
|
||||
const arg = (expr)[k[0]];
|
||||
const arg = fnArgs;
|
||||
if (['string', 'number'].includes(typeof arg) || arg instanceof Date) {
|
||||
args.push(translateConstant(arg));
|
||||
}
|
||||
|
|
@ -1064,7 +1212,7 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
|
||||
// 新的remove应该包含$$deleteAt$$的值了
|
||||
/* const now = Date.now();
|
||||
|
||||
|
||||
const updateText2 = updateText ? `${updateText}, \`${alias}\`.\`$$deleteAt$$\` = '${now}'` : `\`${alias}\`.\`$$deleteAt$$\` = '${now}'`; */
|
||||
assert(updateText.includes(DeleteAtAttribute));
|
||||
let sql = `update ${fromText} set ${updateText}`;
|
||||
|
|
@ -1212,4 +1360,4 @@ export class MySqlTranslator<ED extends EntityDict & BaseEntityDict> extends Sql
|
|||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg';
|
||||
import { v4 } from 'uuid';
|
||||
import { TxnOption } from 'oak-domain/lib/types';
|
||||
import { PostgreSQLConfiguration } from './types/Configuration';
|
||||
import assert from 'assert';
|
||||
|
||||
export class PostgreSQLConnector {
|
||||
pool: Pool;
|
||||
configuration: PostgreSQLConfiguration;
|
||||
txnDict: Record<string, PoolClient>;
|
||||
|
||||
constructor(configuration: PostgreSQLConfiguration) {
|
||||
this.configuration = configuration;
|
||||
this.txnDict = {};
|
||||
this.pool = new Pool(configuration);
|
||||
|
||||
// 错误处理
|
||||
this.pool.on('error', (err) => {
|
||||
console.error('PostgreSQL pool error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
connect() {
|
||||
// pg Pool 是惰性连接,不需要显式连接
|
||||
// 但可以在这里进行连接测试
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
// 先获取所有事务ID的快照
|
||||
const txnIds = Object.keys(this.txnDict);
|
||||
|
||||
for (const txnId of txnIds) {
|
||||
try {
|
||||
await this.rollbackTransaction(txnId);
|
||||
} catch (e) {
|
||||
console.error(`Failed to rollback transaction ${txnId}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
await this.pool.end();
|
||||
}
|
||||
|
||||
async startTransaction(option?: TxnOption): Promise<string> {
|
||||
const startInner = async (): Promise<string> => {
|
||||
const connection = await this.pool.connect();
|
||||
|
||||
// 添加:检测连接是否已被其他事务占用
|
||||
for (const txn2 in this.txnDict) {
|
||||
if (this.txnDict[txn2] === connection) {
|
||||
return new Promise((resolve) => {
|
||||
this.pool.on('release', () => resolve(startInner()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let beginStmt = 'BEGIN';
|
||||
|
||||
if (option?.isolationLevel) {
|
||||
// PostgreSQL 隔离级别:
|
||||
// READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
|
||||
// 注意: PostgreSQL 的 READ UNCOMMITTED 行为等同于 READ COMMITTED
|
||||
const level = this.mapIsolationLevel(option.isolationLevel);
|
||||
beginStmt = `BEGIN ISOLATION LEVEL ${level}`;
|
||||
}
|
||||
|
||||
await connection.query(beginStmt);
|
||||
|
||||
const id = v4();
|
||||
this.txnDict[id] = connection;
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
// 如果启动事务失败,释放连接
|
||||
connection.release();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return startInner();
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射隔离级别到 PostgreSQL 语法
|
||||
*/
|
||||
private mapIsolationLevel(level: string): string {
|
||||
const levelUpper = level.toUpperCase().replace(/[-_]/g, ' '); // 同时处理 - 和 _
|
||||
|
||||
const validLevels = [
|
||||
'READ UNCOMMITTED',
|
||||
'READ COMMITTED',
|
||||
'REPEATABLE READ',
|
||||
'SERIALIZABLE'
|
||||
];
|
||||
|
||||
if (validLevels.includes(levelUpper)) {
|
||||
return levelUpper;
|
||||
}
|
||||
|
||||
// 默认使用 READ COMMITTED
|
||||
console.warn(`Unknown isolation level "${level}", using READ COMMITTED`);
|
||||
return 'READ COMMITTED';
|
||||
}
|
||||
|
||||
async exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('SQL:', sql);
|
||||
}
|
||||
|
||||
try {
|
||||
let result: QueryResult;
|
||||
|
||||
if (txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
assert(connection, `Transaction ${txn} not found`);
|
||||
result = await connection.query(sql);
|
||||
} else {
|
||||
result = await this.pool.query(sql);
|
||||
}
|
||||
|
||||
// 返回格式与 mysql2 兼容: [rows, fields/result]
|
||||
return [result.rows, result];
|
||||
} catch (error: any) {
|
||||
// 增强错误信息
|
||||
const enhancedError = new Error(
|
||||
`PostgreSQL query failed: ${error.message}\nSQL: ${sql.slice(0, 500)}${sql.length > 500 ? '...' : ''}`
|
||||
);
|
||||
(enhancedError as any).originalError = error;
|
||||
(enhancedError as any).sql = sql;
|
||||
throw enhancedError;
|
||||
}
|
||||
}
|
||||
|
||||
async commitTransaction(txn: string): Promise<void> {
|
||||
const connection = this.txnDict[txn];
|
||||
assert(connection, `Transaction ${txn} not found`);
|
||||
|
||||
try {
|
||||
await connection.query('COMMIT');
|
||||
} finally {
|
||||
delete this.txnDict[txn];
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
async rollbackTransaction(txn: string): Promise<void> {
|
||||
const connection = this.txnDict[txn];
|
||||
assert(connection, `Transaction ${txn} not found`);
|
||||
|
||||
try {
|
||||
await connection.query('ROLLBACK');
|
||||
} finally {
|
||||
delete this.txnDict[txn];
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行多条 SQL 语句(用于初始化等场景)
|
||||
*/
|
||||
async execBatch(sqls: string[], txn?: string): Promise<void> {
|
||||
for (const sql of sqls) {
|
||||
if (sql.trim()) {
|
||||
await this.exec(sql, txn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接池状态
|
||||
*/
|
||||
getPoolStatus(): { total: number; idle: number; waiting: number } {
|
||||
return {
|
||||
total: this.pool.totalCount,
|
||||
idle: this.pool.idleCount,
|
||||
waiting: this.pool.waitingCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
import {
|
||||
EntityDict,
|
||||
OperateOption,
|
||||
OperationResult,
|
||||
TxnOption,
|
||||
StorageSchema,
|
||||
SelectOption,
|
||||
AggregationResult,
|
||||
Geo
|
||||
} from 'oak-domain/lib/types';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { PostgreSQLConfiguration } from './types/Configuration';
|
||||
import { PostgreSQLConnector } from './connector';
|
||||
import { PostgreSQLTranslator, PostgreSQLSelectOption, PostgreSQLOperateOption } from './translator';
|
||||
import { assign, set } from 'lodash';
|
||||
import assert from 'assert';
|
||||
import { judgeRelation } from 'oak-domain/lib/store/relation';
|
||||
import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { CreateEntityOption } from '../types/Translator';
|
||||
import { QueryResult } from 'pg';
|
||||
import { DbStore } from '../types/dbStore';
|
||||
|
||||
function convertGeoTextToObject(geoText: string): Geo {
|
||||
if (geoText.startsWith('POINT')) {
|
||||
const coord = geoText.match(/(-?\d+\.?\d*)/g) as string[];
|
||||
|
||||
assert(coord && coord.length === 2);
|
||||
return {
|
||||
type: 'point',
|
||||
coordinate: coord.map(ele => parseFloat(ele)) as [number, number],
|
||||
};
|
||||
} else if (geoText.startsWith('LINESTRING')) {
|
||||
const coordsMatch = geoText.match(/\(([^)]+)\)/);
|
||||
if (coordsMatch) {
|
||||
const points = coordsMatch[1].split(',').map(p => {
|
||||
const [x, y] = p.trim().split(/\s+/).map(parseFloat);
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
return {
|
||||
type: 'path',
|
||||
coordinate: points,
|
||||
};
|
||||
}
|
||||
} else if (geoText.startsWith('POLYGON')) {
|
||||
const ringsMatch = geoText.match(/\(\(([^)]+)\)\)/g);
|
||||
if (ringsMatch) {
|
||||
const rings = ringsMatch.map(ring => {
|
||||
const coordStr = ring.replace(/[()]/g, '');
|
||||
return coordStr.split(',').map(p => {
|
||||
const [x, y] = p.trim().split(/\s+/).map(parseFloat);
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
});
|
||||
return {
|
||||
type: 'polygon',
|
||||
coordinate: rings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported geometry type: ${geoText.slice(0, 50)}`);
|
||||
}
|
||||
|
||||
export class PostgreSQLStore<
|
||||
ED extends EntityDict & BaseEntityDict,
|
||||
Cxt extends AsyncContext<ED>
|
||||
> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
||||
|
||||
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(
|
||||
entity: T,
|
||||
selection: Pick<ED[T]['Selection'], 'filter' | 'count'>,
|
||||
context: Cxt,
|
||||
option: OP
|
||||
): number {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
|
||||
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(
|
||||
entity: T,
|
||||
aggregation: ED[T]['Aggregation'],
|
||||
context: Cxt,
|
||||
option: OP
|
||||
): AggregationResult<ED[T]['Schema']> {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
|
||||
protected selectAbjointRow<T extends keyof ED, OP extends SelectOption>(
|
||||
entity: T,
|
||||
selection: ED[T]['Selection'],
|
||||
context: SyncContext<ED>,
|
||||
option: OP
|
||||
): Partial<ED[T]['Schema']>[] {
|
||||
throw new Error('PostgreSQL store 不支持同步取数据');
|
||||
}
|
||||
|
||||
protected updateAbjointRow<T extends keyof ED, OP extends OperateOption>(
|
||||
entity: T,
|
||||
operation: ED[T]['Operation'],
|
||||
context: SyncContext<ED>,
|
||||
option: OP
|
||||
): number {
|
||||
throw new Error('PostgreSQL store 不支持同步更新数据');
|
||||
}
|
||||
|
||||
async exec(script: string, txnId?: string) {
|
||||
await this.connector.exec(script, txnId);
|
||||
}
|
||||
|
||||
connector: PostgreSQLConnector;
|
||||
translator: PostgreSQLTranslator<ED>;
|
||||
|
||||
constructor(storageSchema: StorageSchema<ED>, configuration: PostgreSQLConfiguration) {
|
||||
super(storageSchema);
|
||||
this.connector = new PostgreSQLConnector(configuration);
|
||||
this.translator = new PostgreSQLTranslator(storageSchema);
|
||||
}
|
||||
|
||||
checkRelationAsync<T extends keyof ED, Cxt extends AsyncContext<ED>>(
|
||||
entity: T,
|
||||
operation: Omit<ED[T]['Operation'] | ED[T]['Selection'], 'id'>,
|
||||
context: Cxt
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected async aggregateAbjointRowAsync<T extends keyof ED, OP extends SelectOption, Cxt extends AsyncContext<ED>>(
|
||||
entity: T,
|
||||
aggregation: ED[T]['Aggregation'],
|
||||
context: Cxt,
|
||||
option: OP
|
||||
): Promise<AggregationResult<ED[T]['Schema']>> {
|
||||
const sql = this.translator.translateAggregate(entity, aggregation, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
return this.formResult(entity, result[0]);
|
||||
}
|
||||
|
||||
aggregate<T extends keyof ED, OP extends SelectOption>(
|
||||
entity: T,
|
||||
aggregation: ED[T]['Aggregation'],
|
||||
context: Cxt,
|
||||
option: OP
|
||||
): Promise<AggregationResult<ED[T]['Schema']>> {
|
||||
return this.aggregateAsync(entity, aggregation, context, option);
|
||||
}
|
||||
|
||||
protected supportManyToOneJoin(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected supportMultipleCreate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private formResult<T extends keyof ED>(entity: T, result: any): any {
|
||||
const schema = this.getSchema();
|
||||
|
||||
function resolveAttribute<E extends keyof ED>(
|
||||
entity2: E,
|
||||
r: Record<string, any>,
|
||||
attr: string,
|
||||
value: any
|
||||
) {
|
||||
const { attributes, view } = schema[entity2];
|
||||
|
||||
if (!view) {
|
||||
const i = attr.indexOf(".");
|
||||
|
||||
if (i !== -1) {
|
||||
const attrHead = attr.slice(0, i);
|
||||
const attrTail = attr.slice(i + 1);
|
||||
const rel = judgeRelation(schema, entity2, attrHead);
|
||||
|
||||
if (rel === 1) {
|
||||
set(r, attr, value);
|
||||
} else {
|
||||
if (!r[attrHead]) {
|
||||
r[attrHead] = {};
|
||||
}
|
||||
|
||||
if (rel === 0) {
|
||||
resolveAttribute(entity2, r[attrHead], attrTail, value);
|
||||
} else if (rel === 2) {
|
||||
resolveAttribute(attrHead, r[attrHead], attrTail, value);
|
||||
} else {
|
||||
assert(typeof rel === 'string');
|
||||
resolveAttribute(rel, r[attrHead], attrTail, value);
|
||||
}
|
||||
}
|
||||
} else if (attributes[attr]) {
|
||||
const { type } = attributes[attr];
|
||||
|
||||
switch (type) {
|
||||
case 'date':
|
||||
case 'time': {
|
||||
if (value instanceof Date) {
|
||||
r[attr] = value.valueOf();
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'geometry': {
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = convertGeoTextToObject(value);
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'object':
|
||||
case 'array': {
|
||||
// PostgreSQL jsonb 直接返回对象,不需要 parse
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = JSON.parse(value);
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = `return ${Buffer.from(value, 'base64').toString()}`;
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'bool':
|
||||
case 'boolean': {
|
||||
// PostgreSQL 直接返回 boolean 类型
|
||||
r[attr] = value;
|
||||
break;
|
||||
}
|
||||
case 'decimal': {
|
||||
// PostgreSQL numeric 类型可能返回字符串
|
||||
if (typeof value === 'string') {
|
||||
r[attr] = parseFloat(value);
|
||||
} else {
|
||||
assert(value === null || typeof value === 'number');
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: 这里和mysql统一行为,ref类型的字符串去除前后空格
|
||||
case "char":
|
||||
case "ref": {
|
||||
if (value) {
|
||||
assert(typeof value === 'string');
|
||||
r[attr] = value.trim();
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
r[attr] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: 这里和mysql统一行为,id字段为char类型时,去除后面的空格
|
||||
if (value && typeof value === 'string') {
|
||||
if (attr === 'id') {
|
||||
r[attr] = value.trim();
|
||||
} else if (attr.startsWith("#count")) {
|
||||
// PostgreSQL count 返回字符串
|
||||
r[attr] = parseInt(value, 10);
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
} else {
|
||||
r[attr] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assign(r, { [attr]: value });
|
||||
}
|
||||
}
|
||||
|
||||
function removeNullObjects<E extends keyof ED>(r: Record<string, any>, e: E) {
|
||||
for (let attr in r) {
|
||||
const rel = judgeRelation(schema, e, attr);
|
||||
|
||||
if (rel === 2) {
|
||||
if (r[attr].id === null) {
|
||||
assert(schema[e].toModi || r.entity !== attr);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
removeNullObjects(r[attr], attr);
|
||||
} else if (typeof rel === 'string') {
|
||||
if (r[attr].id === null) {
|
||||
assert(
|
||||
schema[e].toModi || r[`${attr}Id`] === null,
|
||||
`对象${String(e)}取数据时,发现其外键找不到目标对象,rowId是${r.id},其外键${attr}Id值为${r[`${attr}Id`]}`
|
||||
);
|
||||
delete r[attr];
|
||||
continue;
|
||||
}
|
||||
removeNullObjects(r[attr], rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formSingleRow(r: any): any {
|
||||
let result2: Record<string, any> = {};
|
||||
|
||||
for (let attr in r) {
|
||||
const value = r[attr];
|
||||
resolveAttribute(entity, result2, attr, value);
|
||||
}
|
||||
|
||||
removeNullObjects(result2, entity);
|
||||
return result2;
|
||||
}
|
||||
|
||||
if (result instanceof Array) {
|
||||
return result.map(r => formSingleRow(r));
|
||||
}
|
||||
return formSingleRow(result);
|
||||
}
|
||||
|
||||
protected async selectAbjointRowAsync<T extends keyof ED>(
|
||||
entity: T,
|
||||
selection: ED[T]['Selection'],
|
||||
context: AsyncContext<ED>,
|
||||
option?: PostgreSQLSelectOption
|
||||
): Promise<Partial<ED[T]['Schema']>[]> {
|
||||
const sql = this.translator.translateSelect(entity, selection, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
return this.formResult(entity, result[0]);
|
||||
}
|
||||
|
||||
protected async updateAbjointRowAsync<T extends keyof ED>(
|
||||
entity: T,
|
||||
operation: ED[T]['Operation'],
|
||||
context: AsyncContext<ED>,
|
||||
option?: PostgreSQLOperateOption
|
||||
): Promise<number> {
|
||||
const { translator, connector } = this;
|
||||
const { action } = operation;
|
||||
const txn = context.getCurrentTxnId();
|
||||
|
||||
switch (action) {
|
||||
case 'create': {
|
||||
const { data } = operation as ED[T]['Create'];
|
||||
const sql = translator.translateInsert(entity, data instanceof Array ? data : [data]);
|
||||
const result = await connector.exec(sql, txn);
|
||||
// PostgreSQL QueryResult.rowCount
|
||||
return (result[1] as QueryResult).rowCount || 0;
|
||||
}
|
||||
case 'remove': {
|
||||
const sql = translator.translateRemove(entity, operation as ED[T]['Remove'], option);
|
||||
const result = await connector.exec(sql, txn);
|
||||
return (result[1] as QueryResult).rowCount || 0;
|
||||
}
|
||||
default: {
|
||||
assert(!['select', 'download', 'stat'].includes(action));
|
||||
const sql = translator.translateUpdate(entity, operation as ED[T]['Update'], option);
|
||||
const result = await connector.exec(sql, txn);
|
||||
return (result[1] as QueryResult).rowCount || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async operate<T extends keyof ED>(
|
||||
entity: T,
|
||||
operation: ED[T]['Operation'],
|
||||
context: Cxt,
|
||||
option: OperateOption
|
||||
): Promise<OperationResult<ED>> {
|
||||
const { action } = operation;
|
||||
assert(!['select', 'download', 'stat'].includes(action), '不支持使用 select operation');
|
||||
return await super.operateAsync(entity, operation as any, context, option);
|
||||
}
|
||||
|
||||
async select<T extends keyof ED>(
|
||||
entity: T,
|
||||
selection: ED[T]['Selection'],
|
||||
context: Cxt,
|
||||
option: SelectOption
|
||||
): Promise<Partial<ED[T]['Schema']>[]> {
|
||||
return await super.selectAsync(entity, selection, context, option);
|
||||
}
|
||||
|
||||
protected async countAbjointRowAsync<T extends keyof ED>(
|
||||
entity: T,
|
||||
selection: Pick<ED[T]['Selection'], 'filter' | 'count'>,
|
||||
context: AsyncContext<ED>,
|
||||
option: SelectOption
|
||||
): Promise<number> {
|
||||
const sql = this.translator.translateCount(entity, selection, option);
|
||||
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
||||
|
||||
// PostgreSQL 返回的 count 是 string 类型(bigint)
|
||||
const cnt = result[0][0]?.cnt;
|
||||
return typeof cnt === 'string' ? parseInt(cnt, 10) : (cnt || 0);
|
||||
}
|
||||
|
||||
async count<T extends keyof ED>(
|
||||
entity: T,
|
||||
selection: Pick<ED[T]['Selection'], 'filter' | 'count'>,
|
||||
context: Cxt,
|
||||
option: SelectOption
|
||||
) {
|
||||
return this.countAsync(entity, selection, context, option);
|
||||
}
|
||||
|
||||
async begin(option?: TxnOption): Promise<string> {
|
||||
return await this.connector.startTransaction(option);
|
||||
}
|
||||
|
||||
async commit(txnId: string): Promise<void> {
|
||||
await this.connector.commitTransaction(txnId);
|
||||
}
|
||||
|
||||
async rollback(txnId: string): Promise<void> {
|
||||
await this.connector.rollbackTransaction(txnId);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.connector.connect();
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.connector.disconnect();
|
||||
}
|
||||
|
||||
async initialize(option: CreateEntityOption) {
|
||||
const schema = this.getSchema();
|
||||
|
||||
// 可选:先创建 PostGIS 扩展
|
||||
// await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
|
||||
|
||||
for (const entity in schema) {
|
||||
const sqls = this.translator.translateCreateEntity(entity, option);
|
||||
for (const sql of sqls) {
|
||||
await this.connector.exec(sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
|||
export type PostgreSQLConfiguration = {
|
||||
host: string;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
port?: number;
|
||||
max?: number; // connection pool size
|
||||
idleTimeoutMillis?: number;
|
||||
connectionTimeoutMillis?: number;
|
||||
}
|
||||
|
||||
export type Configuration = {
|
||||
postgresql: PostgreSQLConfiguration;
|
||||
}
|
||||
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export * from './MySQL/store';
|
||||
export { MySqlSelectOption, MysqlOperateOption} from './MySQL/translator';
|
||||
export { MySqlSelectOption, MysqlOperateOption} from './MySQL/translator';
|
||||
export * from './PostgreSQL/store';
|
||||
export { PostgreSQLSelectOption, PostgreSQLOperateOption } from './PostgreSQL/translator';
|
||||
|
|
@ -237,7 +237,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
expression: RefOrExpression<keyof ED[T]['OpSchema']>,
|
||||
refDict: Record<string, [string, keyof ED]>): string;
|
||||
|
||||
private getStorageName<T extends keyof ED>(entity: T) {
|
||||
protected getStorageName<T extends keyof ED>(entity: T) {
|
||||
const { storageName } = this.schema[entity];
|
||||
return (storageName || entity) as string;
|
||||
}
|
||||
|
|
@ -246,7 +246,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
const { schema } = this;
|
||||
const { attributes, storageName = entity } = schema[entity];
|
||||
|
||||
let sql = `insert into \`${storageName as string}\`(`;
|
||||
let sql = `insert into ${this.quoteIdentifier(storageName as string)}(`;
|
||||
|
||||
/**
|
||||
* 这里的attrs要用所有行的union集合
|
||||
|
|
@ -257,7 +257,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
);
|
||||
attrs.forEach(
|
||||
(attr, idx) => {
|
||||
sql += ` \`${attr}\``;
|
||||
sql += ` ${this.quoteIdentifier(attr)}`;
|
||||
if (idx < attrs.length - 1) {
|
||||
sql += ',';
|
||||
}
|
||||
|
|
@ -327,7 +327,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
const filterRefAlias: Record<string, [string, keyof ED]> = {};
|
||||
|
||||
const alias = `${entity as string}_${number++}`;
|
||||
let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
|
||||
let from = ` ${this.quoteIdentifier(this.getStorageName(entity))} ${this.quoteIdentifier(alias)} `;
|
||||
const aliasDict: Record<string, string> = {
|
||||
'./': alias,
|
||||
};
|
||||
|
|
@ -371,7 +371,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${op}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(op + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -391,7 +391,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(op)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${op}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(op))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${op}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -443,7 +443,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -463,7 +463,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${attr}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(attr))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${attr}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -512,7 +512,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(rel))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr + 'Id')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')}`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -534,7 +534,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
assign(aliasDict, {
|
||||
[pathAttr]: alias2,
|
||||
});
|
||||
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\` and \`${alias}\`.\`entity\` = '${attr}'`;
|
||||
from += ` left join ${this.quoteIdentifier(this.getStorageName(attr))} ${this.quoteIdentifier(alias2)} on ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entityId')} = ${this.quoteIdentifier(alias2)}.${this.quoteIdentifier('id')} and ${this.quoteIdentifier(alias)}.${this.quoteIdentifier('entity')} = '${attr}'`;
|
||||
}
|
||||
else {
|
||||
alias2 = aliasDict[pathAttr];
|
||||
|
|
@ -580,10 +580,18 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对like模式中的特殊字符进行转义
|
||||
* 例如 % 和 _,以防止被当成通配符处理
|
||||
* @param pattern like模式字符串
|
||||
* @returns 转义后的字符串
|
||||
*/
|
||||
escapeLikePattern(pattern: string): string {
|
||||
return pattern.replace(/[%_]/g, (match) => `\\${match}`);
|
||||
}
|
||||
|
||||
private translateComparison(attr: string, value: any, type?: DataType | Ref): string {
|
||||
const SQL_OP: {
|
||||
[op: string]: string,
|
||||
} = {
|
||||
const SQL_OP: { [op: string]: string } = {
|
||||
$gt: '>',
|
||||
$lt: '<',
|
||||
$gte: '>=',
|
||||
|
|
@ -603,16 +611,19 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
|
||||
switch (attr) {
|
||||
case '$startsWith': {
|
||||
return ` like '${value}%'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '${escaped}%'`;
|
||||
}
|
||||
case '$endsWith': {
|
||||
return ` like '%${value}'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '%${escaped}'`;
|
||||
}
|
||||
case '$includes': {
|
||||
return ` like '%${value}%'`;
|
||||
const escaped = this.escapeLikePattern(value);
|
||||
return ` LIKE '%${escaped}%'`;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unrecoganized comparison operator ${attr}`);
|
||||
throw new Error(`unrecognized comparison operator ${attr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -663,7 +674,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
const values = value.map(
|
||||
(v: string | number) => {
|
||||
if (type && ['varchar', 'char', 'text', 'nvarchar', 'ref', 'enum'].includes(type as string) || typeof v === 'string') {
|
||||
return `'${v}'`;
|
||||
return this.escapeStringValue(String(v));
|
||||
}
|
||||
else {
|
||||
return `${v}`;
|
||||
|
|
@ -682,7 +693,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
const values = value.map(
|
||||
(v: string | number) => {
|
||||
if (type && ['varchar', 'char', 'text', 'nvarchar', 'ref', 'enum'].includes(type as string) || typeof v === 'string') {
|
||||
return `'${v}'`;
|
||||
return this.escapeStringValue(String(v));
|
||||
}
|
||||
else {
|
||||
return `${v}`;
|
||||
|
|
@ -705,7 +716,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
}
|
||||
}
|
||||
|
||||
private translateFilter<T extends keyof ED, OP extends SqlSelectOption>(
|
||||
protected translateFilter<T extends keyof ED, OP extends SqlSelectOption>(
|
||||
entity: T,
|
||||
filter: ED[T]['Selection']['filter'],
|
||||
aliasDict: Record<string, string>,
|
||||
|
|
@ -814,7 +825,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 {
|
||||
/**
|
||||
|
|
@ -872,15 +883,19 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
whereText += `(${this.translateObjectPredicate(filter2[attr], alias, attr)})`;
|
||||
}
|
||||
else {
|
||||
if (!filter2[attr]) {
|
||||
throw new Error(`属性${attr}的查询条件不能为null或undefined`);
|
||||
}
|
||||
|
||||
assert(Object.keys(filter2[attr]).length === 1);
|
||||
const predicate = Object.keys(filter2[attr])[0];
|
||||
assert(predicate.startsWith('$'));
|
||||
// 对属性上的谓词处理
|
||||
whereText += ` (\`${alias}\`.\`${attr}\` ${this.translatePredicate(predicate, filter2[attr][predicate], type2)})`;
|
||||
whereText += ` (${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} ${this.translatePredicate(predicate, filter2[attr][predicate], type2)})`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
whereText += ` (\`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])})`;
|
||||
whereText += ` (${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${this.translateAttrValue(type2, filter2[attr])})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -911,7 +926,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
return this.translateExpression(entity2, alias, sortAttr[attr] as any, {});
|
||||
}
|
||||
else if (sortAttr[attr] === 1) {
|
||||
return `\`${alias}\`.\`${attr}\``;
|
||||
return `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)}`;
|
||||
}
|
||||
else {
|
||||
const rel = judgeRelation(this.schema, entity2, attr);
|
||||
|
|
@ -979,12 +994,12 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
projText += ` ${exprText}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${exprText} as \`${prefix2}${attr}\``;
|
||||
projText += ` ${exprText} as ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${attr}\``;
|
||||
as = this.quoteIdentifier(prefix2 + attr);
|
||||
}
|
||||
else {
|
||||
as += `, \`${prefix2}${attr}\``;
|
||||
as += `, ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1003,12 +1018,12 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix2}${attr}\``;
|
||||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${attr}\``;
|
||||
as = this.quoteIdentifier(prefix2 + attr);
|
||||
}
|
||||
else {
|
||||
as += `, \`${prefix2}${attr}\``;
|
||||
as += `, ${this.quoteIdentifier(prefix2 + attr)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1024,12 +1039,12 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)}`;
|
||||
}
|
||||
else {
|
||||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix2}${projection2[attr]}\``;
|
||||
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as ${this.quoteIdentifier(`${prefix2}${projection2[attr]}`)}`;
|
||||
if (!as) {
|
||||
as = `\`${prefix2}${projection2[attr]}\``;
|
||||
as = this.quoteIdentifier(`${prefix2}${projection2[attr]}`);
|
||||
}
|
||||
else {
|
||||
as += `\`${prefix2}${projection2[attr]}\``;
|
||||
as += `, ${this.quoteIdentifier(`${prefix2}${projection2[attr]}`)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1115,29 +1130,29 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
let { projText: projSubText } = this.translateProjection(entity, (data as any)[k]!, aliasDict, projectionRefAlias, undefined, true);
|
||||
let projSubText2 = '';
|
||||
if (k.startsWith('#max')) {
|
||||
projSubText2 = `max(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `max(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#min')) {
|
||||
projSubText2 = `min(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `min(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#count')) {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
projSubText2 = `count(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `count(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else if (k.startsWith('#sum')) {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
projSubText2 = `sum(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `sum(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
else {
|
||||
if (data.distinct) {
|
||||
projSubText = `distinct ${projSubText}`;
|
||||
}
|
||||
assert(k.startsWith('#avg'));
|
||||
projSubText2 = `avg(${projSubText}) as \`${k}\``;
|
||||
projSubText2 = `avg(${projSubText}) as ${this.quoteIdentifier(k)}`;
|
||||
}
|
||||
if (!projText) {
|
||||
projText = projSubText2;
|
||||
|
|
@ -1198,7 +1213,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
// delete只支持对volatile trigger的metadata域赋值
|
||||
assert([TriggerDataAttribute, TriggerUuidAttribute, DeleteAtAttribute, UpdateAtAttribute].includes(attr));
|
||||
const value = this.translateAttrValue(attributes[attr].type as DataType, data[attr]);
|
||||
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
|
||||
updateText += `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${value}`;
|
||||
}
|
||||
}
|
||||
return this.populateRemoveStmt(updateText, fromText, aliasDict, filterText, /* sorterText */ undefined, indexFrom, count, option);
|
||||
|
|
@ -1219,7 +1234,7 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
}
|
||||
assert(attributes.hasOwnProperty(attr));
|
||||
const value = this.translateAttrValue(attributes[attr].type as DataType, data[attr]);
|
||||
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
|
||||
updateText += `${this.quoteIdentifier(alias)}.${this.quoteIdentifier(attr)} = ${value}`;
|
||||
}
|
||||
|
||||
const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, option);
|
||||
|
|
@ -1234,10 +1249,10 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
|
||||
let sql: string;
|
||||
if (view) {
|
||||
sql = `drop view if exists \`${storageName}\``;
|
||||
sql = `drop view if exists ${this.quoteIdentifier(storageName)}`;
|
||||
}
|
||||
else {
|
||||
sql = truncate ? `truncate table \`${storageName}\`` : `drop table if exists \`${storageName}\``;
|
||||
sql = truncate ? `truncate table ${this.quoteIdentifier(storageName)}` : `drop table if exists ${this.quoteIdentifier(storageName)}`;
|
||||
}
|
||||
|
||||
return sql;
|
||||
|
|
@ -1256,4 +1271,8 @@ export abstract class SqlTranslator<ED extends EntityDict & BaseEntityDict> {
|
|||
|
||||
return sql1.replaceAll(reg, '') === sql2.replaceAll(reg, '');
|
||||
}
|
||||
}
|
||||
|
||||
quoteIdentifier(name: string): string {
|
||||
return `\`${name}\``; // MySQL 默认
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { PostgreSQLConfiguration } from './../PostgreSQL/types/Configuration';
|
||||
import { MySQLConfiguration } from './../MySQL/types/Configuration';
|
||||
|
||||
export type DbConfiguration = PostgreSQLConfiguration & {
|
||||
type: 'postgresql';
|
||||
} | MySQLConfiguration & {
|
||||
type: 'mysql';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { EntityDict } from "oak-domain/lib/base-app-domain";
|
||||
import { AsyncContext,AsyncRowStore } from "oak-domain/lib/store/AsyncRowStore";
|
||||
import { CreateEntityOption } from "./Translator";
|
||||
|
||||
export interface DbStore<ED extends EntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize(options: CreateEntityOption): Promise<void>;
|
||||
};
|
||||
|
|
@ -10,7 +10,7 @@ export interface Schema extends EntityShape {
|
|||
area: Area;
|
||||
owner: User;
|
||||
dd: Array<ExtraFile>;
|
||||
size: Float<4, 2>;
|
||||
size: Float<8, 4>;
|
||||
};
|
||||
|
||||
const locale: LocaleDef<Schema, '', '', {}> = {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,34 @@
|
|||
import { PostgreSQLStore } from '../lib/PostgreSQL/store';
|
||||
import assert from 'assert';
|
||||
import { TestContext } from './Context';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityDict, storageSchema } from './test-app-domain';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { WebConfig } from './test-app-domain/Application/Schema';
|
||||
import { describe, it, before, after } from './utils/test';
|
||||
import { tests } from './testcase';
|
||||
|
||||
describe('test postgresstore', async function () {
|
||||
let store: PostgreSQLStore<EntityDict, TestContext>;
|
||||
|
||||
before(async () => {
|
||||
store = new PostgreSQLStore(storageSchema, {
|
||||
host: 'localhost',
|
||||
database: 'oakdb',
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
max: 20,
|
||||
port: 5432,
|
||||
});
|
||||
store.connect();
|
||||
await store.initialize({
|
||||
ifExists: 'drop',
|
||||
});
|
||||
});
|
||||
|
||||
tests(() => store!)
|
||||
|
||||
after(() => {
|
||||
store.disconnect();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it } from 'mocha';
|
||||
import { MySqlTranslator } from '../src/MySQL/translator';
|
||||
import { MySqlTranslator } from '../lib/MySQL/translator';
|
||||
import { EntityDict, storageSchema } from './test-app-domain';
|
||||
|
||||
describe('test MysqlTranslator', function () {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,108 @@
|
|||
type AsyncFunc = () => Promise<void>;
|
||||
type SyncFunc = () => void;
|
||||
|
||||
const beforeFuncs: AsyncFunc[] = [];
|
||||
const itFuncs: { desc: string; fn: AsyncFunc }[] = [];
|
||||
const afterFuncs: SyncFunc[] = [];
|
||||
|
||||
const stats = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
errors: [] as {
|
||||
type: 'before' | 'it' | 'after';
|
||||
desc?: string;
|
||||
error: unknown;
|
||||
}[],
|
||||
};
|
||||
|
||||
export const before = (func: AsyncFunc) => {
|
||||
beforeFuncs.push(func);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const it = (desc: string, func: AsyncFunc) => {
|
||||
itFuncs.push({ desc, fn: func });
|
||||
};
|
||||
|
||||
export const after = (func: SyncFunc) => {
|
||||
afterFuncs.push(func);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const describe = async (desc: string, func: () => Promise<void> | void) => {
|
||||
console.log(desc);
|
||||
|
||||
// 注册阶段
|
||||
await func();
|
||||
|
||||
// before
|
||||
for (const f of beforeFuncs) {
|
||||
try {
|
||||
await f();
|
||||
} catch (err) {
|
||||
stats.failed++;
|
||||
stats.errors.push({
|
||||
type: 'before',
|
||||
error: err,
|
||||
});
|
||||
console.error(' ✗ before failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// it
|
||||
for (const { desc, fn } of itFuncs) {
|
||||
stats.total++;
|
||||
try {
|
||||
console.log(' ' + desc);
|
||||
await fn();
|
||||
stats.passed++;
|
||||
console.log(' ✓ passed');
|
||||
} catch (err) {
|
||||
stats.failed++;
|
||||
stats.errors.push({
|
||||
type: 'it',
|
||||
desc,
|
||||
error: err,
|
||||
});
|
||||
console.error(' ✗ failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// after(无论如何都执行)
|
||||
for (const f of afterFuncs) {
|
||||
try {
|
||||
f();
|
||||
} catch (err) {
|
||||
stats.failed++;
|
||||
stats.errors.push({
|
||||
type: 'after',
|
||||
error: err,
|
||||
});
|
||||
console.error(' ✗ after failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 统计输出
|
||||
printSummary();
|
||||
};
|
||||
|
||||
const printSummary = () => {
|
||||
console.log('\n====== Test Summary ======');
|
||||
console.log(`Total: ${stats.total}`);
|
||||
console.log(`Passed: ${stats.passed}`);
|
||||
console.log(`Failed: ${stats.failed}`);
|
||||
|
||||
if (stats.errors.length) {
|
||||
console.log('\nErrors:');
|
||||
for (const e of stats.errors) {
|
||||
console.log(
|
||||
`- [${e.type}]${e.desc ? ' ' + e.desc : ''}`,
|
||||
'\n ',
|
||||
e.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('==========================');
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"target": "ESNext",
|
||||
"declaration": true,
|
||||
"allowJs": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"outDir": "test-dist", /* Redirect output structure to the directory. */
|
||||
"rootDir": "test", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"types": [
|
||||
"node",
|
||||
"mocha"
|
||||
],
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"test/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue