This commit is contained in:
Xu Chang 2026-01-01 13:44:30 +08:00
commit e9059b8514
39 changed files with 11233 additions and 2399 deletions

5
.gitignore vendored
View File

@ -115,4 +115,7 @@ dist
.yarn/install-state.gz
.pnp.*
package-lock.json
test/test-app-domain
test/test-app-domain
test-dist

34
lib/MySQL/store.d.ts vendored
View File

@ -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>>;
/**
* dataSchemaschemaupgrade
* 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 {};

View File

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

View File

@ -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>>;
}

View File

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

31
lib/PostgreSQL/connector.d.ts vendored Normal file
View File

@ -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;
};
}

157
lib/PostgreSQL/connector.js Normal file
View File

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

38
lib/PostgreSQL/store.d.ts vendored Normal file
View File

@ -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>;
}

329
lib/PostgreSQL/store.js Normal file
View File

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

83
lib/PostgreSQL/translator.d.ts vendored Normal file
View File

@ -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;
}

1769
lib/PostgreSQL/translator.js Normal file

File diff suppressed because it is too large Load Diff

13
lib/PostgreSQL/types/Configuration.d.ts vendored Normal file
View File

@ -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;
};

View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

2
lib/index.d.ts vendored
View File

@ -1,2 +1,4 @@
export * from './MySQL/store';
export { MySqlSelectOption, MysqlOperateOption } from './MySQL/translator';
export * from './PostgreSQL/store';
export { PostgreSQLSelectOption, PostgreSQLOperateOption } from './PostgreSQL/translator';

View File

@ -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);

View File

@ -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;
}

View File

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

7
lib/types/configuration.d.ts vendored Normal file
View File

@ -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';
};

View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

8
lib/types/dbStore.d.ts vendored Normal file
View File

@ -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>;
}

3
lib/types/dbStore.js Normal file
View File

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
;

View File

@ -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"
}
}

View File

@ -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();

View File

@ -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;
}
}
}

179
src/PostgreSQL/connector.ts Normal file
View File

@ -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
};
}
}

443
src/PostgreSQL/store.ts Normal file
View File

@ -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);
}
}
}
}

2040
src/PostgreSQL/translator.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -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';

View File

@ -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 默认
}
}

View File

@ -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';
}

9
src/types/dbStore.ts Normal file
View File

@ -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>;
};

View File

@ -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

34
test/testPostgresStore.ts Normal file
View File

@ -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();
});
});

View File

@ -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 () {

5117
test/testcase/index.ts Normal file

File diff suppressed because it is too large Load Diff

108
test/utils/test.ts Normal file
View File

@ -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('==========================');
};

32
tsconfig.test.json Normal file
View File

@ -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"
]
}