This commit is contained in:
parent
81eb7246f2
commit
acb90279b4
|
|
@ -114,3 +114,5 @@ dist
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
package-lock.json
|
||||||
|
test/test-app-domain
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extension": ["ts"],
|
||||||
|
"spec": "test/test*.ts",
|
||||||
|
"require": "ts-node/register"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "oak-db",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "oak-db",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha",
|
||||||
|
"make:test:domain": "ts-node script/makeTestDomain.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"mysql2": "^2.3.3",
|
||||||
|
"oak-domain": "file:../oak-domain",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"author": "XuChang",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.14.182",
|
||||||
|
"@types/mocha": "^9.1.1",
|
||||||
|
"@types/node": "^17.0.42",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"mocha": "^10.0.0",
|
||||||
|
"oak-general-business": "file:../oak-general-business",
|
||||||
|
"ts-node": "^10.8.1",
|
||||||
|
"typescript": "^4.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {
|
||||||
|
buildSchema,
|
||||||
|
analyzeEntities,
|
||||||
|
} from 'oak-domain/src/compiler/schemalBuilder';
|
||||||
|
|
||||||
|
analyzeEntities(`${process.cwd()}/node_modules/oak-general-business/src/entities`);
|
||||||
|
analyzeEntities(`${process.cwd()}/test/entities`);
|
||||||
|
buildSchema(`${process.cwd()}/test/test-app-domain`);
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import mysql from 'mysql2';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { TxnOption } from 'oak-domain/lib/types';
|
||||||
|
import { MySQLConfiguration } from './types/Configuration';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
export class MySqlConnector {
|
||||||
|
pool?: mysql.Pool;
|
||||||
|
configuration: MySQLConfiguration;
|
||||||
|
txnDict: Record<string, mysql.PoolConnection>;
|
||||||
|
|
||||||
|
constructor(configuration: MySQLConfiguration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.txnDict = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.pool = mysql.createPool(this.configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.pool!.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTransaction(option?: TxnOption): Promise<string> {
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
this.pool!.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
const { isolationLevel } = option || {};
|
||||||
|
const startTxn = () => {
|
||||||
|
let sql = 'START TRANSACTION;';
|
||||||
|
connection.query(sql, (err2: Error) => {
|
||||||
|
if (err2) {
|
||||||
|
connection.release();
|
||||||
|
return reject(err2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = v4();
|
||||||
|
Object.assign(this.txnDict, {
|
||||||
|
[id]: connection,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isolationLevel) {
|
||||||
|
connection.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel};`, (err2: Error) => {
|
||||||
|
if (err2) {
|
||||||
|
connection.release();
|
||||||
|
return reject(err2);
|
||||||
|
}
|
||||||
|
startTxn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startTxn();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(sql: string, txn?: string): Promise<any> {
|
||||||
|
if (txn) {
|
||||||
|
const connection = this.txnDict[txn];
|
||||||
|
assert(connection);
|
||||||
|
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
connection.query(sql, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`sql exec err: ${sql}`, err);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
// if (process.env.DEBUG) {
|
||||||
|
// console.log(sql);
|
||||||
|
//}
|
||||||
|
this.pool!.query(sql, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`sql exec err: ${sql}`, err);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commitTransaction(txn: string): Promise<void> {
|
||||||
|
const connection = this.txnDict[txn];
|
||||||
|
assert(connection);
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
connection.query('COMMIT;', (err) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
connection.release();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rollbackTransaction(txn: string): Promise<void> {
|
||||||
|
const connection = this.txnDict[txn];
|
||||||
|
assert(connection);
|
||||||
|
return new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
connection.query('ROLLBACK;', (err: Error) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
connection.release();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { EntityDict, Context, DeduceCreateSingleOperation, DeduceRemoveOperation, DeduceUpdateOperation, OperateParams, OperationResult, SelectionResult, TxnOption, SelectRowShape, StorageSchema } from 'oak-domain/lib/types';
|
||||||
|
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||||
|
import { MySQLConfiguration } from './types/Configuration';
|
||||||
|
import { MySqlConnector } from './connector';
|
||||||
|
import { translateCreateDatabase } from './Translator';
|
||||||
|
|
||||||
|
export class MysqlStore<ED extends EntityDict, Cxt extends Context<ED>> extends CascadeStore<ED, Cxt> {
|
||||||
|
connector: MySqlConnector;
|
||||||
|
constructor(storageSchema: StorageSchema<ED>, configuration: MySQLConfiguration) {
|
||||||
|
super(storageSchema);
|
||||||
|
this.connector = new MySqlConnector(configuration);
|
||||||
|
}
|
||||||
|
protected supportManyToOneJoin(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected selectAbjointRow<T extends keyof ED, S extends ED[T]['Selection']>(entity: T, Selection: S, context: Cxt, params?: OperateParams): Promise<SelectRowShape<ED[T]['Schema'], S['data']>[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
protected updateAbjointRow<T extends keyof ED>(entity: T, operation: DeduceCreateSingleOperation<ED[T]['Schema']> | DeduceUpdateOperation<ED[T]['Schema']> | DeduceRemoveOperation<ED[T]['Schema']>, context: Cxt, params?: OperateParams): Promise<number> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, params?: OperateParams): Promise<OperationResult<ED>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
select<T extends keyof ED, S extends ED[T]['Selection']>(entity: T, selection: S, context: Cxt, params?: Object): Promise<SelectionResult<ED[T]['Schema'], S['data']>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
count<T extends keyof ED>(entity: T, selection: Omit<ED[T]['Selection'], 'data' | 'sorter' | 'action'>, context: Cxt, params?: Object): Promise<number> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
begin(option?: TxnOption): Promise<string> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
commit(txnId: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
rollback(txnId: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
this.connector.connect();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.connector.disconnect();
|
||||||
|
}
|
||||||
|
async initialize(dropIfExists?: boolean) {
|
||||||
|
const sql = translateCreateDatabase(this.connector.configuration.database, this.connector.configuration.charset, dropIfExists);
|
||||||
|
for (const stmt of sql) {
|
||||||
|
await this.connector.exec(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,589 @@
|
||||||
|
import assert from 'assert';
|
||||||
|
import { assign } from 'lodash';
|
||||||
|
import { EntityDict, Geo, Q_FullTextValue, RefOrExpression, Ref, StorageSchema, Index } from "oak-domain/lib/types";
|
||||||
|
import { DataType, DataTypeParams } from "oak-domain/lib/types/schema/DataTypes";
|
||||||
|
import { SelectParams, SqlTranslator } from "../sqlTranslator";
|
||||||
|
|
||||||
|
const GeoTypes = [
|
||||||
|
{
|
||||||
|
type: 'point',
|
||||||
|
name: "Point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'path',
|
||||||
|
name: "LineString",
|
||||||
|
element: 'point',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultiLineString",
|
||||||
|
element: "path",
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'polygon',
|
||||||
|
name: "Polygon",
|
||||||
|
element: "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultiPoint",
|
||||||
|
element: "point",
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultiPolygon",
|
||||||
|
element: "polygon",
|
||||||
|
multiple: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function transformGeoData(data: Geo): string {
|
||||||
|
if (data instanceof Array) {
|
||||||
|
const element = data[0];
|
||||||
|
if (element instanceof Array) {
|
||||||
|
return ` GeometryCollection(${data.map(
|
||||||
|
ele => transformGeoData(ele)
|
||||||
|
).join(',')})`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const geoType = GeoTypes.find(
|
||||||
|
ele => ele.type === element.type
|
||||||
|
);
|
||||||
|
if (!geoType) {
|
||||||
|
throw new Error(`${element.type} is not supported in MySQL`);
|
||||||
|
}
|
||||||
|
const multiGeoType = GeoTypes.find(
|
||||||
|
ele => ele.element === geoType.type && ele.multiple
|
||||||
|
);
|
||||||
|
return ` ${multiGeoType!.name}(${data.map(
|
||||||
|
ele => transformGeoData(ele)
|
||||||
|
).join(',')})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const { type, coordinate } = data;
|
||||||
|
const geoType = GeoTypes.find(
|
||||||
|
ele => ele.type === type
|
||||||
|
);
|
||||||
|
if (!geoType) {
|
||||||
|
throw new Error(`${data.type} is not supported in MySQL`);
|
||||||
|
}
|
||||||
|
const { element, name } = geoType;
|
||||||
|
if (!element) {
|
||||||
|
// Point
|
||||||
|
return ` ${name}(${coordinate.join(',')})`;
|
||||||
|
}
|
||||||
|
// Polygon or Linestring
|
||||||
|
return ` ${name}(${coordinate.map(
|
||||||
|
(ele) => transformGeoData({
|
||||||
|
type: element as any,
|
||||||
|
coordinate: ele as any,
|
||||||
|
})
|
||||||
|
)})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexHint = {
|
||||||
|
$force?: string;
|
||||||
|
$ignore?: string;
|
||||||
|
} & {
|
||||||
|
[k: string]: IndexHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MySqlSelectParams extends SelectParams {
|
||||||
|
indexHint?: IndexHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
|
||||||
|
private modifySchema() {
|
||||||
|
for (const entity in this.schema) {
|
||||||
|
const { attributes, indexes } = this.schema[entity];
|
||||||
|
const geoIndexes: Index<ED[keyof ED]['OpSchema']>[] = [];
|
||||||
|
for (const attr in attributes) {
|
||||||
|
if (attributes[attr].type === 'geometry') {
|
||||||
|
const geoIndex = indexes?.find(
|
||||||
|
(idx) => idx.config?.type === 'spatial' && idx.attributes.find(
|
||||||
|
(attrDef) => attrDef.name === attr
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!geoIndex) {
|
||||||
|
geoIndexes.push({
|
||||||
|
name: `${entity}_geo_${attr}`,
|
||||||
|
attributes: [{
|
||||||
|
name: attr,
|
||||||
|
}],
|
||||||
|
config: {
|
||||||
|
type: 'spatial',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geoIndexes.length > 0) {
|
||||||
|
if (indexes) {
|
||||||
|
indexes.push(...geoIndexes);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assign(this.schema[entity], {
|
||||||
|
indexes: geoIndexes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(schema: StorageSchema<ED>) {
|
||||||
|
super(schema);
|
||||||
|
// MySQL为geometry属性默认创建索引
|
||||||
|
this.modifySchema();
|
||||||
|
}
|
||||||
|
static supportedDataTypes: DataType[] = [
|
||||||
|
// numeric types
|
||||||
|
"bit",
|
||||||
|
"int",
|
||||||
|
"integer", // synonym for int
|
||||||
|
"tinyint",
|
||||||
|
"smallint",
|
||||||
|
"mediumint",
|
||||||
|
"bigint",
|
||||||
|
"float",
|
||||||
|
"double",
|
||||||
|
"double precision", // synonym for double
|
||||||
|
"real", // synonym for double
|
||||||
|
"decimal",
|
||||||
|
"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",
|
||||||
|
"timestamp",
|
||||||
|
"time",
|
||||||
|
"year",
|
||||||
|
// string types
|
||||||
|
"char",
|
||||||
|
"nchar", // synonym for national char
|
||||||
|
"national char",
|
||||||
|
"varchar",
|
||||||
|
"nvarchar", // synonym for national varchar
|
||||||
|
"national varchar",
|
||||||
|
"blob",
|
||||||
|
"text",
|
||||||
|
"tinyblob",
|
||||||
|
"tinytext",
|
||||||
|
"mediumblob",
|
||||||
|
"mediumtext",
|
||||||
|
"longblob",
|
||||||
|
"longtext",
|
||||||
|
"enum",
|
||||||
|
"set",
|
||||||
|
"binary",
|
||||||
|
"varbinary",
|
||||||
|
// json data type
|
||||||
|
"json",
|
||||||
|
// spatial data types
|
||||||
|
"geometry",
|
||||||
|
"point",
|
||||||
|
"linestring",
|
||||||
|
"polygon",
|
||||||
|
"multipoint",
|
||||||
|
"multilinestring",
|
||||||
|
"multipolygon",
|
||||||
|
"geometrycollection"
|
||||||
|
];
|
||||||
|
|
||||||
|
static spatialTypes: DataType[] = [
|
||||||
|
"geometry",
|
||||||
|
"point",
|
||||||
|
"linestring",
|
||||||
|
"polygon",
|
||||||
|
"multipoint",
|
||||||
|
"multilinestring",
|
||||||
|
"multipolygon",
|
||||||
|
"geometrycollection"
|
||||||
|
];
|
||||||
|
|
||||||
|
static withLengthDataTypes: DataType[] = [
|
||||||
|
"char",
|
||||||
|
"varchar",
|
||||||
|
"nvarchar",
|
||||||
|
"binary",
|
||||||
|
"varbinary"
|
||||||
|
];
|
||||||
|
|
||||||
|
static withPrecisionDataTypes: DataType[] = [
|
||||||
|
"decimal",
|
||||||
|
"dec",
|
||||||
|
"numeric",
|
||||||
|
"fixed",
|
||||||
|
"float",
|
||||||
|
"double",
|
||||||
|
"double precision",
|
||||||
|
"real",
|
||||||
|
"time",
|
||||||
|
"datetime",
|
||||||
|
"timestamp"
|
||||||
|
];
|
||||||
|
|
||||||
|
static withScaleDataTypes: DataType[] = [
|
||||||
|
"decimal",
|
||||||
|
"dec",
|
||||||
|
"numeric",
|
||||||
|
"fixed",
|
||||||
|
"float",
|
||||||
|
"double",
|
||||||
|
"double precision",
|
||||||
|
"real"
|
||||||
|
];
|
||||||
|
|
||||||
|
static unsignedAndZerofillTypes: DataType[] = [
|
||||||
|
"int",
|
||||||
|
"integer",
|
||||||
|
"smallint",
|
||||||
|
"tinyint",
|
||||||
|
"mediumint",
|
||||||
|
"bigint",
|
||||||
|
"decimal",
|
||||||
|
"dec",
|
||||||
|
"numeric",
|
||||||
|
"fixed",
|
||||||
|
"float",
|
||||||
|
"double",
|
||||||
|
"double precision",
|
||||||
|
"real"
|
||||||
|
];
|
||||||
|
|
||||||
|
static withWidthDataTypes: DataType[] = [
|
||||||
|
'int',
|
||||||
|
]
|
||||||
|
|
||||||
|
static dataTypeDefaults = {
|
||||||
|
"varchar": { length: 255 },
|
||||||
|
"nvarchar": { length: 255 },
|
||||||
|
"national varchar": { length: 255 },
|
||||||
|
"char": { length: 1 },
|
||||||
|
"binary": { length: 1 },
|
||||||
|
"varbinary": { length: 255 },
|
||||||
|
"decimal": { precision: 10, scale: 0 },
|
||||||
|
"dec": { precision: 10, scale: 0 },
|
||||||
|
"numeric": { precision: 10, scale: 0 },
|
||||||
|
"fixed": { precision: 10, scale: 0 },
|
||||||
|
"float": { precision: 12 },
|
||||||
|
"double": { precision: 22 },
|
||||||
|
"time": { precision: 0 },
|
||||||
|
"datetime": { precision: 0 },
|
||||||
|
"timestamp": { precision: 0 },
|
||||||
|
"bit": { width: 1 },
|
||||||
|
"int": { width: 11 },
|
||||||
|
"integer": { width: 11 },
|
||||||
|
"tinyint": { width: 4 },
|
||||||
|
"smallint": { width: 6 },
|
||||||
|
"mediumint": { width: 9 },
|
||||||
|
"bigint": { width: 20 }
|
||||||
|
};
|
||||||
|
|
||||||
|
maxAliasLength = 63;
|
||||||
|
private populateDataTypeDef(type: DataType | Ref, params?: DataTypeParams): string{
|
||||||
|
if (MySqlTranslator.withLengthDataTypes.includes(type as DataType)) {
|
||||||
|
if (params) {
|
||||||
|
const { length } = params;
|
||||||
|
return `${type}(${length}) `;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const { length } = (MySqlTranslator.dataTypeDefaults as any)[type];
|
||||||
|
return `${type}(${length}) `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MySqlTranslator.withPrecisionDataTypes.includes(type as DataType)) {
|
||||||
|
if (params) {
|
||||||
|
const { precision, scale } = params;
|
||||||
|
if (typeof scale === 'number') {
|
||||||
|
return `${type}(${precision}, ${scale}) `;
|
||||||
|
}
|
||||||
|
return `${type}(${precision})`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const { precision, scale } = (MySqlTranslator.dataTypeDefaults as any)[type];
|
||||||
|
if (typeof scale === 'number') {
|
||||||
|
return `${type}(${precision}, ${scale}) `;
|
||||||
|
}
|
||||||
|
return `${type}(${precision})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MySqlTranslator.withWidthDataTypes.includes(type as DataType)) {
|
||||||
|
assert(type === 'int');
|
||||||
|
const { width } = params!;
|
||||||
|
switch(width!) {
|
||||||
|
case 1: {
|
||||||
|
return 'tinyint';
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
return 'smallint';
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return 'mediumint';
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return 'int';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'bigint';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['date'].includes(type)) {
|
||||||
|
return 'bigint '; // 因为历史原因,date类型用bigint存,Date.now()
|
||||||
|
}
|
||||||
|
if (['object', 'array'].includes(type)) {
|
||||||
|
return 'text ';
|
||||||
|
}
|
||||||
|
if (['image', 'function'].includes(type)) {
|
||||||
|
return 'text ';
|
||||||
|
}
|
||||||
|
if (type === 'ref') {
|
||||||
|
return 'char(36)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${type} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected translateAttrProjection(dataType: DataType, alias: string, attr: string): string {
|
||||||
|
switch(dataType) {
|
||||||
|
case 'geometry': {
|
||||||
|
return ` st_astext(\`${alias}\`.\`${attr}\`)`;
|
||||||
|
}
|
||||||
|
default:{
|
||||||
|
return ` \`${alias}\`.\`${attr}\``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected translateAttrValue(dataType: DataType, value: any): string {
|
||||||
|
if (value === null) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
switch (dataType) {
|
||||||
|
case 'geometry': {
|
||||||
|
return transformGeoData(value);
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return `${value.valueOf()}`;
|
||||||
|
}
|
||||||
|
else if (typeof value === 'number') {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
case 'object':
|
||||||
|
case 'array': {
|
||||||
|
return this.escapeStringValue(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
/* case 'function': {
|
||||||
|
return `'${Buffer.from(value.toString()).toString('base64')}'`;
|
||||||
|
} */
|
||||||
|
default: {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return this.escapeStringValue(value);
|
||||||
|
}
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string {
|
||||||
|
const { $search } = value;
|
||||||
|
const { indexes } = this.schema[entity];
|
||||||
|
|
||||||
|
const ftIndex = indexes && indexes.find(
|
||||||
|
(ele) => {
|
||||||
|
const { config } = ele;
|
||||||
|
return config && config.type === 'fulltext';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert(ftIndex);
|
||||||
|
const { attributes } = ftIndex;
|
||||||
|
const columns2 = attributes.map(
|
||||||
|
({ name }) => `${alias}.${name as string}`
|
||||||
|
);
|
||||||
|
return ` match(${columns2.join(',')}) against ('${$search}' in natural language mode)`;
|
||||||
|
}
|
||||||
|
translateCreateEntity<T extends keyof ED>(entity: T, options?: { replace?: boolean; }): string {
|
||||||
|
const replace = options?.replace;
|
||||||
|
const { schema } = this;
|
||||||
|
const entityDef = schema[entity];
|
||||||
|
const { storageName, attributes, indexes, view } = entityDef;
|
||||||
|
|
||||||
|
// todo view暂还不支持
|
||||||
|
const entityType = view ? 'view' : 'table';
|
||||||
|
let sql = !replace ? `create ${entityType} if not exists ` : `create ${entityType} `;
|
||||||
|
if (storageName) {
|
||||||
|
sql += `\`${storageName}\` `;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sql += `\`${entity as string}\` `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view) {
|
||||||
|
throw new Error(' view unsupported yet');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sql += '(';
|
||||||
|
// 翻译所有的属性
|
||||||
|
Object.keys(attributes).forEach(
|
||||||
|
(attr, idx) => {
|
||||||
|
const attrDef = attributes[attr];
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
params,
|
||||||
|
default: defaultValue,
|
||||||
|
unique,
|
||||||
|
notNull,
|
||||||
|
} = attrDef;
|
||||||
|
if (type === 'ref') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sql += `\`${attr}\` `
|
||||||
|
sql += this.populateDataTypeDef(type, params) as string;
|
||||||
|
|
||||||
|
if (notNull) {
|
||||||
|
sql += ' not null ';
|
||||||
|
}
|
||||||
|
if (unique) {
|
||||||
|
sql += ' unique ';
|
||||||
|
}
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
|
||||||
|
}
|
||||||
|
if (attr === 'id') {
|
||||||
|
sql += ' primary key'
|
||||||
|
}
|
||||||
|
if (idx < Object.keys(attributes).length - 1) {
|
||||||
|
sql += ',\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 翻译索引信息
|
||||||
|
if (indexes) {
|
||||||
|
sql += ',\n';
|
||||||
|
indexes.forEach(
|
||||||
|
({ name, attributes, config }, idx) => {
|
||||||
|
const { unique, type, parser } = config || {};
|
||||||
|
if (unique) {
|
||||||
|
sql += ' unique ';
|
||||||
|
}
|
||||||
|
else if (type === 'fulltext') {
|
||||||
|
sql += ' fulltext ';
|
||||||
|
}
|
||||||
|
else if (type === 'spatial') {
|
||||||
|
sql += ' spatial ';
|
||||||
|
}
|
||||||
|
sql += `index ${name} `;
|
||||||
|
if (type === 'hash') {
|
||||||
|
sql += ` using hash `;
|
||||||
|
}
|
||||||
|
sql += '(';
|
||||||
|
|
||||||
|
let includeDeleteAt = false;
|
||||||
|
attributes.forEach(
|
||||||
|
({ name, size, direction }, idx2) => {
|
||||||
|
sql += `\`${name as string}\``;
|
||||||
|
if (size) {
|
||||||
|
sql += ` (${size})`;
|
||||||
|
}
|
||||||
|
if (direction) {
|
||||||
|
sql += ` ${direction}`;
|
||||||
|
}
|
||||||
|
if (idx2 < attributes.length - 1) {
|
||||||
|
sql += ','
|
||||||
|
}
|
||||||
|
if (name === '$$deleteAt$$') {
|
||||||
|
includeDeleteAt = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!includeDeleteAt && !type) {
|
||||||
|
sql += ', $$deleteAt$$';
|
||||||
|
}
|
||||||
|
sql += ')';
|
||||||
|
if (parser) {
|
||||||
|
sql += ` with parser ${parser}`;
|
||||||
|
}
|
||||||
|
if (idx < indexes.length - 1) {
|
||||||
|
sql += ',\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sql += ')';
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
protected translateExpression<T extends keyof ED>(alias: string, expression: RefOrExpression<keyof ED[T]["OpSchema"]>, refDict: Record<string, string>): string {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
protected populateSelectStmt(projectionText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, params?: MySqlSelectParams): string {
|
||||||
|
// todo using index
|
||||||
|
let sql = `select ${projectionText} from ${fromText}`;
|
||||||
|
if (filterText) {
|
||||||
|
sql += ` where ${filterText}`;
|
||||||
|
}
|
||||||
|
if (sorterText) {
|
||||||
|
sql += ` order by ${sorterText}`;
|
||||||
|
}
|
||||||
|
if (typeof indexFrom === 'number') {
|
||||||
|
assert (typeof count === 'number');
|
||||||
|
sql += ` limit ${indexFrom}, ${count}`;
|
||||||
|
}
|
||||||
|
if (params?.forUpdate) {
|
||||||
|
sql += ' for update';
|
||||||
|
}
|
||||||
|
sql += ';';
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
protected populateUpdateStmt(updateText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, params?: MySqlSelectParams): string {
|
||||||
|
// todo using index
|
||||||
|
const alias = aliasDict['./'];
|
||||||
|
let sql = `update ${fromText} set ${updateText}, \`${alias}\`.\`$$updateAt$$\` = ${Date.now()}`;
|
||||||
|
if (filterText) {
|
||||||
|
sql += ` where ${filterText}`;
|
||||||
|
}
|
||||||
|
if (sorterText) {
|
||||||
|
sql += ` order by ${sorterText}`;
|
||||||
|
}
|
||||||
|
if (typeof indexFrom === 'number') {
|
||||||
|
assert (typeof count === 'number');
|
||||||
|
sql += ` limit ${indexFrom}, ${count}`;
|
||||||
|
}
|
||||||
|
sql += ';';
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
protected populateRemoveStmt(removeText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, params?: MySqlSelectParams): string {
|
||||||
|
// todo using index
|
||||||
|
const alias = aliasDict['./'];
|
||||||
|
let sql = `update ${fromText} set \`${alias}\`.\`$$removeAt$$\` = ${Date.now()}`;
|
||||||
|
if (filterText) {
|
||||||
|
sql += ` where ${filterText}`;
|
||||||
|
}
|
||||||
|
if (sorterText) {
|
||||||
|
sql += ` order by ${sorterText}`;
|
||||||
|
}
|
||||||
|
if (typeof indexFrom === 'number') {
|
||||||
|
assert (typeof count === 'number');
|
||||||
|
sql += ` limit ${indexFrom}, ${count}`;
|
||||||
|
}
|
||||||
|
sql += ';';
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type MySQLConfiguration = {
|
||||||
|
host: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
database: string;
|
||||||
|
charset: 'utf8mb4_general_ci';
|
||||||
|
connectionLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Configuration = {
|
||||||
|
mysql: MySQLConfiguration;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,831 @@
|
||||||
|
import assert from 'assert';
|
||||||
|
import { assign, cloneDeep, keys, set } from 'lodash';
|
||||||
|
import { Attribute, DeduceCreateOperationData, DeduceSorterAttr, DeduceSorterItem, EntityDict, Expression, EXPRESSION_PREFIX, Index, Q_FullTextValue, RefOrExpression, StorageSchema } from "oak-domain/lib/types";
|
||||||
|
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
|
||||||
|
import { judgeRelation } from 'oak-domain/lib/store/relation';
|
||||||
|
|
||||||
|
export type SelectParams = {
|
||||||
|
forUpdate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class SqlTranslator<ED extends EntityDict> {
|
||||||
|
readonly schema: StorageSchema<ED>;
|
||||||
|
constructor(schema: StorageSchema<ED>) {
|
||||||
|
this.schema = this.makeFullSchema(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeFullSchema(schema2: StorageSchema<ED>) {
|
||||||
|
const schema = cloneDeep(schema2);
|
||||||
|
for (const entity in schema) {
|
||||||
|
const { attributes, indexes } = schema[entity];
|
||||||
|
// 增加默认的属性
|
||||||
|
assign(attributes, {
|
||||||
|
id: {
|
||||||
|
type: 'char',
|
||||||
|
params: {
|
||||||
|
length: 36,
|
||||||
|
},
|
||||||
|
} as Attribute,
|
||||||
|
$$createAt$$: {
|
||||||
|
type: 'date',
|
||||||
|
notNull: true,
|
||||||
|
} as Attribute,
|
||||||
|
$$updateAt$$: {
|
||||||
|
type: 'date',
|
||||||
|
notNull: true,
|
||||||
|
} as Attribute,
|
||||||
|
$$removeAt$$: {
|
||||||
|
type: 'date',
|
||||||
|
} as Attribute,
|
||||||
|
$$triggerData$$: {
|
||||||
|
type: 'object',
|
||||||
|
} as Attribute,
|
||||||
|
$$triggerTimestamp$$: {
|
||||||
|
type: 'date',
|
||||||
|
} as Attribute,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 增加默认的索引
|
||||||
|
const intrinsticIndexes: Index<ED[keyof ED]['OpSchema']>[] = [
|
||||||
|
{
|
||||||
|
name: `${entity}_create_at`,
|
||||||
|
attributes: [{
|
||||||
|
name: '$$createAt$$',
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
name: `${entity}_update_at`,
|
||||||
|
attributes: [{
|
||||||
|
name: '$$updateAt$$',
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
name: `${entity}_trigger_ts`,
|
||||||
|
attributes: [{
|
||||||
|
name: '$$triggerTimestamp$$',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 增加外键上的索引
|
||||||
|
for (const attr in attributes) {
|
||||||
|
if (attributes[attr].type === 'ref') {
|
||||||
|
if (!(indexes?.find(
|
||||||
|
ele => ele.attributes[0].name === attr
|
||||||
|
))) {
|
||||||
|
intrinsticIndexes.push({
|
||||||
|
name: `${entity}_fk_${attr}`,
|
||||||
|
attributes: [{
|
||||||
|
name: attr,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr === 'entity' && attributes[attr].type === 'varchar') {
|
||||||
|
const entityIdDef = attributes.entityId;
|
||||||
|
if (entityIdDef?.type === 'varchar') {
|
||||||
|
if (!(indexes?.find(
|
||||||
|
ele => ele.attributes[0].name === 'entity' && ele.attributes[1]?.name === 'entityId'
|
||||||
|
))) {
|
||||||
|
intrinsticIndexes.push({
|
||||||
|
name: `${entity}_fk_entity_entityId`,
|
||||||
|
attributes: [{
|
||||||
|
name: 'entity',
|
||||||
|
}, {
|
||||||
|
name: 'entityId',
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexes) {
|
||||||
|
indexes.push(...intrinsticIndexes);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assign(schema[entity], {
|
||||||
|
indexes: intrinsticIndexes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract translateAttrProjection(dataType: DataType, alias: string, attr: string): string;
|
||||||
|
|
||||||
|
protected abstract translateAttrValue(dataType: DataType, value: any): string;
|
||||||
|
|
||||||
|
protected abstract translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string;
|
||||||
|
|
||||||
|
abstract translateCreateEntity<T extends keyof ED>(entity: T, option: { replace?: boolean }): string;
|
||||||
|
|
||||||
|
protected abstract populateSelectStmt(
|
||||||
|
projectionText: string,
|
||||||
|
fromText: string,
|
||||||
|
aliasDict: Record<string, string>,
|
||||||
|
filterText: string,
|
||||||
|
sorterText?: string,
|
||||||
|
indexFrom?: number,
|
||||||
|
count?: number,
|
||||||
|
params?: SelectParams): string;
|
||||||
|
|
||||||
|
protected abstract populateUpdateStmt(
|
||||||
|
updateText: string,
|
||||||
|
fromText: string,
|
||||||
|
aliasDict: Record<string, string>,
|
||||||
|
filterText: string,
|
||||||
|
sorterText?: string,
|
||||||
|
indexFrom?: number,
|
||||||
|
count?: number,
|
||||||
|
params?: any): string;
|
||||||
|
|
||||||
|
protected abstract populateRemoveStmt(
|
||||||
|
removeText: string,
|
||||||
|
fromText: string,
|
||||||
|
aliasDict: Record<string, string>,
|
||||||
|
filterText: string,
|
||||||
|
sorterText?: string,
|
||||||
|
indexFrom?: number,
|
||||||
|
count?: number,
|
||||||
|
params?: any): string;
|
||||||
|
|
||||||
|
protected abstract translateExpression<T extends keyof ED>(alias: string, expression: RefOrExpression<keyof ED[T]['OpSchema']>, refDict: Record<string, string>): string;
|
||||||
|
|
||||||
|
private getStorageName<T extends keyof ED>(entity: T) {
|
||||||
|
const { storageName } = this.schema[entity];
|
||||||
|
return (storageName || entity) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
translateInsert<T extends keyof ED>(entity: T, data: DeduceCreateOperationData<ED[T]['OpSchema']>[]): string {
|
||||||
|
const { schema } = this;
|
||||||
|
const { attributes, storageName = entity } = schema[entity];
|
||||||
|
|
||||||
|
let sql = `insert into \`${storageName as string}\`(`;
|
||||||
|
|
||||||
|
const attrs = Object.keys(data[0]).filter(
|
||||||
|
ele => attributes.hasOwnProperty(ele) && attributes[ele].type !== 'ref'
|
||||||
|
);
|
||||||
|
attrs.forEach(
|
||||||
|
(attr, idx) => {
|
||||||
|
sql += ` \`${attr}\``;
|
||||||
|
if (idx < attrs.length - 1) {
|
||||||
|
sql += ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
sql += ' `$$createAt$$, $$updateAt$$) values ';
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
data.forEach(
|
||||||
|
(d, dataIndex) => {
|
||||||
|
sql += '(';
|
||||||
|
attrs.forEach(
|
||||||
|
(attr, attrIdx) => {
|
||||||
|
const attrDef = attributes[attr];
|
||||||
|
const { type: dataType } = attrDef;
|
||||||
|
const value = this.translateAttrValue(dataType as DataType, d[attr]);
|
||||||
|
sql += value;
|
||||||
|
if (attrIdx < attrs.length - 1) {
|
||||||
|
sql += ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sql += `, ${now}, ${now})`;
|
||||||
|
if (dataIndex < data.length - 1) {
|
||||||
|
sql += ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* analyze the join relations in projection/query/sort
|
||||||
|
* 所有的层次关系都当成left join处理,如果有内表为空的情况,请手动处理
|
||||||
|
* {
|
||||||
|
* b: {
|
||||||
|
* name: {
|
||||||
|
* $exists: false,
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* 这样的query会把内表为空的行也返回
|
||||||
|
* @param param0
|
||||||
|
*/
|
||||||
|
private analyzeJoin<T extends keyof ED>(entity: T, { projection, filter, sorter, isStat }: {
|
||||||
|
projection?: ED[T]['Selection']['data'];
|
||||||
|
filter?: ED[T]['Selection']['filter'];
|
||||||
|
sorter?: ED[T]['Selection']['sorter'];
|
||||||
|
isStat?: true;
|
||||||
|
}): {
|
||||||
|
aliasDict: Record<string, string>;
|
||||||
|
projectionRefAlias: Record<string, string>;
|
||||||
|
filterRefAlias: Record<string, string>;
|
||||||
|
from: string;
|
||||||
|
extraWhere: string;
|
||||||
|
} {
|
||||||
|
const { schema } = this;
|
||||||
|
let count = 1;
|
||||||
|
const projectionRefAlias: Record<string, string> = {};
|
||||||
|
const filterRefAlias: Record<string, string> = {};
|
||||||
|
let extraWhere = '';
|
||||||
|
|
||||||
|
const alias = `${entity as string}_${count++}`;
|
||||||
|
let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
|
||||||
|
const aliasDict: Record<string, string> = {
|
||||||
|
'./': alias,
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyzeFilterNode = <E extends keyof ED>({ node, path, entityName, alias }: {
|
||||||
|
node: ED[E]['Selection']['filter'];
|
||||||
|
path: string;
|
||||||
|
entityName: E;
|
||||||
|
alias: string,
|
||||||
|
}): void => {
|
||||||
|
Object.keys(node!).forEach(
|
||||||
|
(op) => {
|
||||||
|
if (['$and', '$or'].includes(op)) {
|
||||||
|
(node![op] as ED[E]['Selection']['filter'][]).forEach(
|
||||||
|
(subNode) => analyzeFilterNode({
|
||||||
|
node: subNode,
|
||||||
|
path,
|
||||||
|
entityName,
|
||||||
|
alias,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const rel = judgeRelation(this.schema, entityName, op);
|
||||||
|
if (typeof rel === 'string') {
|
||||||
|
let alias2: string;
|
||||||
|
const pathAttr = `${path}${op}/`;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${rel}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${op}Id\` = \`${alias2}\`.\`id\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
analyzeFilterNode({
|
||||||
|
node: node![op],
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: rel,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (rel === 2) {
|
||||||
|
let alias2: string;
|
||||||
|
const pathAttr = `${path}${op}/`;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${op}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(op)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
|
||||||
|
extraWhere += `\`${alias}\`.\`entity\` = '${op}'`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
analyzeFilterNode({
|
||||||
|
node: node![op],
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: rel,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 不支持一对多
|
||||||
|
assert(rel === 0 || rel === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (node!['#id']) {
|
||||||
|
assert(!filterRefAlias[node!['#id']]);
|
||||||
|
assign(filterRefAlias, {
|
||||||
|
[node!['#id']]: alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (filter) {
|
||||||
|
analyzeFilterNode({
|
||||||
|
node: filter,
|
||||||
|
path: './',
|
||||||
|
entityName: entity,
|
||||||
|
alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeSortNode = <E extends keyof ED>({ node, path, entityName, alias }: {
|
||||||
|
node: DeduceSorterAttr<ED[E]['Schema']>;
|
||||||
|
path: string;
|
||||||
|
entityName: E;
|
||||||
|
alias: string;
|
||||||
|
}): void => {
|
||||||
|
const attr = keys(node)[0];
|
||||||
|
|
||||||
|
const rel = judgeRelation(this.schema, entityName, attr);
|
||||||
|
if (typeof rel === 'string') {
|
||||||
|
const pathAttr = `${path}${attr}/`;
|
||||||
|
let alias2: string;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${rel}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
analyzeSortNode({
|
||||||
|
node: node[attr] as any,
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: rel,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (rel === 2) {
|
||||||
|
const pathAttr = `${path}${attr}/`;
|
||||||
|
let alias2: string;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${attr}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
|
||||||
|
extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
analyzeSortNode({
|
||||||
|
node: node[attr] as any,
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: attr,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert(rel === 0 || rel === 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (sorter) {
|
||||||
|
sorter.forEach(
|
||||||
|
(sortNode) => {
|
||||||
|
analyzeSortNode({
|
||||||
|
node: sortNode.$attr,
|
||||||
|
path: './',
|
||||||
|
entityName: entity,
|
||||||
|
alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeProjectionNode = <E extends keyof ED>({ node, path, entityName, alias }: {
|
||||||
|
node: ED[E]['Selection']['data'];
|
||||||
|
path: string;
|
||||||
|
entityName: E;
|
||||||
|
alias: string;
|
||||||
|
}): void => {
|
||||||
|
const { attributes } = schema[entityName];
|
||||||
|
|
||||||
|
if (!isStat && attributes.hasOwnProperty('id') && !node.id) {
|
||||||
|
assign(node, {
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.keys(node).forEach(
|
||||||
|
(attr) => {
|
||||||
|
const rel = judgeRelation(this.schema, entityName, attr);
|
||||||
|
if (typeof rel === 'string') {
|
||||||
|
const pathAttr = `${path}${attr}/`;
|
||||||
|
|
||||||
|
let alias2: string;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${rel}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeProjectionNode({
|
||||||
|
node: node[attr],
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: rel,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (rel === 2) {
|
||||||
|
const pathAttr = `${path}${attr}/`;
|
||||||
|
|
||||||
|
let alias2: string;
|
||||||
|
if (!aliasDict.hasOwnProperty(pathAttr)) {
|
||||||
|
alias2 = `${attr}_${count++}`;
|
||||||
|
assign(aliasDict, {
|
||||||
|
[pathAttr]: alias2,
|
||||||
|
});
|
||||||
|
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alias2 = aliasDict[pathAttr];
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeProjectionNode({
|
||||||
|
node: node[attr],
|
||||||
|
path: pathAttr,
|
||||||
|
entityName: attr,
|
||||||
|
alias: alias2,
|
||||||
|
});
|
||||||
|
extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (node['#id']) {
|
||||||
|
assert(!projectionRefAlias[node['#id']]);
|
||||||
|
assign(projectionRefAlias, {
|
||||||
|
[node['#id']]: alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projection) {
|
||||||
|
analyzeProjectionNode({ node: projection, path: './', entityName: entity, alias });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
aliasDict,
|
||||||
|
from,
|
||||||
|
projectionRefAlias,
|
||||||
|
filterRefAlias,
|
||||||
|
extraWhere,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateComparison(attr: string, value: any, type: DataType): string {
|
||||||
|
const SQL_OP: {
|
||||||
|
[op: string]: string,
|
||||||
|
} = {
|
||||||
|
$gt: '>',
|
||||||
|
$lt: '<',
|
||||||
|
$gte: '>=',
|
||||||
|
$lte: '<=',
|
||||||
|
$eq: '=',
|
||||||
|
$ne: '<>',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(SQL_OP).includes(attr)) {
|
||||||
|
return ` ${SQL_OP[attr]} ${this.translateAttrValue(type, value)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (attr) {
|
||||||
|
case '$startsWith': {
|
||||||
|
return ` like '${value}%'`;
|
||||||
|
}
|
||||||
|
case '$endsWith': {
|
||||||
|
return ` like '%${value}'`;
|
||||||
|
}
|
||||||
|
case '$includes': {
|
||||||
|
return ` like '%${value}%'`;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`unrecoganized comparison operator ${attr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateElement(attr: string, value: boolean): string {
|
||||||
|
assert(attr === '$exists'); // only support one operator now
|
||||||
|
if (value) {
|
||||||
|
return ' is not null';
|
||||||
|
}
|
||||||
|
return ' is null';
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateEvaluation<T extends keyof ED>(attr: string, value: any, entity: T, alias: string, type: DataType): string {
|
||||||
|
switch (attr) {
|
||||||
|
case '$text': {
|
||||||
|
// fulltext search
|
||||||
|
return this.translateFullTextSearch(value, entity, alias);
|
||||||
|
}
|
||||||
|
case '$in':
|
||||||
|
case '$nin': {
|
||||||
|
const IN_OP = {
|
||||||
|
$in: 'in',
|
||||||
|
$nin: 'not in',
|
||||||
|
};
|
||||||
|
if (value instanceof Array) {
|
||||||
|
const values = value.map(
|
||||||
|
(v) => {
|
||||||
|
if (['varchar', 'char', 'text', 'nvarchar'].includes(type as string)) {
|
||||||
|
return `'${v}'`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return `${v}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (values.length > 0) {
|
||||||
|
return ` ${IN_OP[attr]}(${values.join(',')})`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (attr === '$in') {
|
||||||
|
return ' in (null)';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ' is not null';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// sub query
|
||||||
|
return ` ${IN_OP[attr]}(${this.translateSelect(value.$entity, value)})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assert('$between' === attr);
|
||||||
|
const values = value.map(
|
||||||
|
(v: string | number) => {
|
||||||
|
if (['varchar', 'char', 'text', 'nvarchar'].includes(type as string)) {
|
||||||
|
return `'${v}'`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return `${v}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return ` between ${values[0]} and ${values[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateFilter<T extends keyof ED>(
|
||||||
|
entity: T,
|
||||||
|
aliasDict: Record<string, string>,
|
||||||
|
filterRefAlias: Record<string, string>,
|
||||||
|
filter?: ED[T]['Selection']['filter'],
|
||||||
|
extraWhere?: string): string {
|
||||||
|
const { schema } = this;
|
||||||
|
|
||||||
|
const translateInner = <E extends keyof ED>(entity2: E, path: string, filter2?: ED[E]['Selection']['filter'], type?: DataType): string => {
|
||||||
|
const alias = aliasDict[path];
|
||||||
|
const { attributes } = schema[entity2];
|
||||||
|
let whereText = '';
|
||||||
|
if (filter2) {
|
||||||
|
Object.keys(filter2).forEach(
|
||||||
|
(attr, idx) => {
|
||||||
|
whereText + '(';
|
||||||
|
if (['$and', '$or', '$xor', '$not'].includes(attr)) {
|
||||||
|
let result = '';
|
||||||
|
switch (attr) {
|
||||||
|
case '$and':
|
||||||
|
case '$or':
|
||||||
|
case '$xor': {
|
||||||
|
const logicQueries = filter2[attr];
|
||||||
|
logicQueries.forEach(
|
||||||
|
(logicQuery: ED[E]['Selection']['filter'], index: number) => {
|
||||||
|
const sql = translateInner(entity2, path, logicQuery);
|
||||||
|
if (sql) {
|
||||||
|
whereText += ` (${sql})`;
|
||||||
|
if (index < logicQueries.length - 1) {
|
||||||
|
whereText += ` ${attr.slice(1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assert(attr === '$not');
|
||||||
|
const logicQuery = filter2[attr];
|
||||||
|
const sql = translateInner(entity2, path, logicQuery);
|
||||||
|
if (sql) {
|
||||||
|
whereText += ` not (${translateInner(entity2, path, logicQuery)})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
|
||||||
|
// expression
|
||||||
|
whereText += ` (${this.translateExpression(alias, filter2[attr], filterRefAlias)}) as ${attr}`;
|
||||||
|
}
|
||||||
|
else if (['$gt', '$gte', '$lt', '$lte', '$eq', '$ne', '$startsWith', '$endsWith', '$includes'].includes(attr)) {
|
||||||
|
whereText += this.translateComparison(attr, filter2[attr], type!);
|
||||||
|
}
|
||||||
|
else if (['$exists'].includes(attr)) {
|
||||||
|
whereText += this.translateElement(attr, filter2[attr]);
|
||||||
|
}
|
||||||
|
else if (['$text', '$in', '$nin', '$between'].includes(attr)) {
|
||||||
|
whereText += this.translateEvaluation(attr, filter2[attr], entity2, alias, type!);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert(attributes.hasOwnProperty(attr));
|
||||||
|
const { type: type2, ref } = attributes[attr];
|
||||||
|
if (type2 === 'ref') {
|
||||||
|
whereText += ` ${translateInner(ref!, `${path}${attr}/`, filter2[attr])}`;
|
||||||
|
}
|
||||||
|
else if (typeof filter2[attr] === 'object' && Object.keys(filter2[attr])[0] && Object.keys(filter2[attr])[0].startsWith('$')) {
|
||||||
|
whereText += ` \`${alias}\`.\`${attr}\` ${translateInner(entity2, path, filter2[attr], type2)}`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
whereText += ` \`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
whereText + ')';
|
||||||
|
if (idx < Object.keys(filter2).length - 1) {
|
||||||
|
whereText += ' and'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return whereText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const where = translateInner(entity, './', filter);
|
||||||
|
if (extraWhere && where) {
|
||||||
|
return `${extraWhere} and ${where}`;
|
||||||
|
}
|
||||||
|
return extraWhere || where;
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateSorter<T extends keyof ED>(entity: T, sorter: ED[T]['Selection']['sorter'], aliasDict: Record<string, string>): string {
|
||||||
|
const translateInner = <E extends keyof ED>(entity2: E, sortAttr: DeduceSorterAttr<ED[E]['Schema']>, path: string): string => {
|
||||||
|
assert(Object.keys(sortAttr).length === 1);
|
||||||
|
const attr = Object.keys(sortAttr)[0];
|
||||||
|
const alias = aliasDict[path];
|
||||||
|
|
||||||
|
if (attr.toLocaleLowerCase().startsWith(EXPRESSION_PREFIX)) {
|
||||||
|
return this.translateExpression(alias, sortAttr[attr] as any, {});
|
||||||
|
}
|
||||||
|
else if (sortAttr[attr] === 1) {
|
||||||
|
return `\`${alias}\`.\`${attr}\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const rel = judgeRelation(this.schema, entity2, attr);
|
||||||
|
if (typeof rel === 'string') {
|
||||||
|
return translateInner(rel, sortAttr[attr] as any, `${path}${attr}/`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert(rel === 2);
|
||||||
|
return translateInner(attr, sortAttr[attr] as any, `${path}${attr}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sortText = '';
|
||||||
|
sorter!.forEach(
|
||||||
|
(sortNode, index) => {
|
||||||
|
const { $attr, $direction } = sortNode;
|
||||||
|
sortText += translateInner(entity, $attr, './');
|
||||||
|
if ($direction) {
|
||||||
|
sortText += ` ${$direction}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < sorter!.length - 1) {
|
||||||
|
sortText += ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private translateProjection<T extends keyof ED>(
|
||||||
|
entity: T,
|
||||||
|
projection: ED[T]['Selection']['data'],
|
||||||
|
aliasDict: Record<string, string>,
|
||||||
|
projectionRefAlias: Record<string, string>): string {
|
||||||
|
const { schema } = this;
|
||||||
|
const translateInner = <E extends keyof ED>(entity2: E, projection2: ED[E]['Selection']['data'], path: string): string => {
|
||||||
|
const alias = aliasDict[path];
|
||||||
|
const { attributes } = schema[entity2];
|
||||||
|
let projText = '';
|
||||||
|
|
||||||
|
let prefix = path.slice(2).replace(/\//g, '.');
|
||||||
|
Object.keys(projection2).forEach(
|
||||||
|
(attr, idx) => {
|
||||||
|
if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
|
||||||
|
const exprText = this.translateExpression(alias, projection2[attr], projectionRefAlias);
|
||||||
|
projText += ` ${exprText} as ${prefix}${attr}`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const rel = judgeRelation(this.schema, entity2, attr);
|
||||||
|
if (typeof rel === 'string') {
|
||||||
|
projText += translateInner(rel, projection2[attr], `${path}${attr}/`);
|
||||||
|
}
|
||||||
|
else if (rel === 2) {
|
||||||
|
projText += translateInner(attr, projection2[attr], `${path}${attr}/`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert(rel === 0 || rel === 1);
|
||||||
|
const { type } = attributes[attr];
|
||||||
|
if (projection2[attr] === 1) {
|
||||||
|
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix}${attr}\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert (typeof projection2 === 'string');
|
||||||
|
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix}${projection2[attr]}\``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idx < Object.keys(projection2).length - 1) {
|
||||||
|
projText += ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return projText;
|
||||||
|
};
|
||||||
|
|
||||||
|
return translateInner(entity, projection, './');
|
||||||
|
}
|
||||||
|
|
||||||
|
translateSelect<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], params?: SelectParams): string {
|
||||||
|
const { data, filter, sorter, indexFrom, count } = selection;
|
||||||
|
const { from: fromText, aliasDict, projectionRefAlias, extraWhere, filterRefAlias } = this.analyzeJoin(entity, {
|
||||||
|
projection: data,
|
||||||
|
filter,
|
||||||
|
sorter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const projText = this.translateProjection(entity, data, aliasDict, projectionRefAlias);
|
||||||
|
|
||||||
|
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
|
||||||
|
|
||||||
|
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict) ;
|
||||||
|
|
||||||
|
return this.populateSelectStmt(projText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateRemove<T extends keyof ED>(entity: T, operation: ED[T]['Remove'], params?: SelectParams): string {
|
||||||
|
const { filter, sorter, indexFrom, count } = operation;
|
||||||
|
const { aliasDict, filterRefAlias, extraWhere, from: fromText } = this.analyzeJoin(entity, { filter, sorter });
|
||||||
|
|
||||||
|
const alias = aliasDict['./'];
|
||||||
|
|
||||||
|
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
|
||||||
|
|
||||||
|
const sorterText = sorter && sorter.length > 0 ? this.translateSorter(entity, sorter, aliasDict) : undefined;
|
||||||
|
|
||||||
|
return this.populateRemoveStmt(alias, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateUpdate<T extends keyof ED>(entity: T, operation: ED[T]['Update'], params?: any): string {
|
||||||
|
const { attributes } = this.schema[entity];
|
||||||
|
const { filter, sorter, indexFrom, count, data } = operation;
|
||||||
|
const { aliasDict, filterRefAlias, extraWhere, from: fromText } = this.analyzeJoin(entity, { filter, sorter });
|
||||||
|
|
||||||
|
const alias = aliasDict['./'];
|
||||||
|
|
||||||
|
let updateText = '';
|
||||||
|
for (const attr in data) {
|
||||||
|
if (updateText) {
|
||||||
|
updateText += ',';
|
||||||
|
}
|
||||||
|
assert(attributes.hasOwnProperty(attr) && attributes[attr].type !== 'ref');
|
||||||
|
const value = this.translateAttrValue(attributes[attr].type as DataType, data[attr]);
|
||||||
|
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
|
||||||
|
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
|
||||||
|
|
||||||
|
return this.populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
translateDestroyEntity(entity: string, truncate?: boolean): string {
|
||||||
|
const { schema } = this;
|
||||||
|
const { storageName = entity, view } = schema[entity];
|
||||||
|
|
||||||
|
let sql: string;
|
||||||
|
if (view) {
|
||||||
|
sql = `drop view if exists \`${storageName}\``;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sql = truncate ? `truncate table \`${storageName}\`` : `drop table if exists \`${storageName}\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
escapeStringValue(value: string): string {
|
||||||
|
const result = `'${value.replace(/'/g, '\\\'')}'`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { String, Int, Datetime, Image, Boolean, Text } from 'oak-domain/lib/types/DataType';
|
||||||
|
import { Schema as Area } from 'oak-general-business/lib/entities/Area';
|
||||||
|
import { Schema as User } from 'oak-general-business/lib/entities/User';
|
||||||
|
import { Schema as ExtraFile } from 'oak-general-business/lib/entities/ExtraFile';
|
||||||
|
import { EntityShape } from 'oak-domain/lib/types';
|
||||||
|
import { LocaleDef } from 'oak-domain/lib/types/Locale';
|
||||||
|
|
||||||
|
export interface Schema extends EntityShape {
|
||||||
|
district: String<16>;
|
||||||
|
area: Area;
|
||||||
|
owner: User;
|
||||||
|
dd: Array<ExtraFile>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const locale: LocaleDef<Schema, '', '', {}> = {
|
||||||
|
zh_CN: {
|
||||||
|
attr: {
|
||||||
|
district: '街区',
|
||||||
|
area: '地区',
|
||||||
|
owner: '房主',
|
||||||
|
dd: '文件'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { MySqlTranslator } from '../src/MySQL/translator';
|
||||||
|
import { EntityDict, storageSchema } from './test-app-domain';
|
||||||
|
|
||||||
|
describe('test MysqlTranslator', function() {
|
||||||
|
this.timeout(100000);
|
||||||
|
let translator: MySqlTranslator<EntityDict>;
|
||||||
|
before(() => {
|
||||||
|
translator = new MySqlTranslator(storageSchema);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test create', () => {
|
||||||
|
const sql = translator.translateCreateEntity('token');
|
||||||
|
console.log(sql);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test insert', () => {
|
||||||
|
const sql = translator.translateInsert('user', [{
|
||||||
|
id: 'xcxc',
|
||||||
|
name: 'xc',
|
||||||
|
nickname: 'xcxcxc',
|
||||||
|
}, {
|
||||||
|
id: 'gggg',
|
||||||
|
name: 'gg',
|
||||||
|
nickname: 'gggggg',
|
||||||
|
}]);
|
||||||
|
// console.log(sql);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test select', () => {
|
||||||
|
const sql = translator.translateSelect('token', {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
$$createAt$$: 1,
|
||||||
|
userId: 1,
|
||||||
|
mobile: {
|
||||||
|
id: 1,
|
||||||
|
mobile: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// console.log(sql);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "esnext",
|
||||||
|
"allowJs": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"outDir": "lib", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "src", /* 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": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue