This commit is contained in:
parent
81eb7246f2
commit
acb90279b4
|
|
@ -114,3 +114,5 @@ dist
|
|||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.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