709 lines
22 KiB
JavaScript
709 lines
22 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.MySqlTranslator = void 0;
|
|
const assert_1 = __importDefault(require("assert"));
|
|
const util_1 = require("util");
|
|
const lodash_1 = require("lodash");
|
|
const luxon_1 = require("luxon");
|
|
const sqlTranslator_1 = require("../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) {
|
|
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,
|
|
coordinate: ele,
|
|
}))})`;
|
|
}
|
|
}
|
|
class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
getDefaultSelectFilter(alias, hint) {
|
|
if (hint?.includeDeleted) {
|
|
return '';
|
|
}
|
|
return ` \`${alias}\`.\`$$deleteAt$$\` is null`;
|
|
}
|
|
modifySchema() {
|
|
for (const entity in this.schema) {
|
|
const { attributes, indexes } = this.schema[entity];
|
|
const geoIndexes = [];
|
|
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 {
|
|
(0, lodash_1.assign)(this.schema[entity], {
|
|
indexes: geoIndexes,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
constructor(schema) {
|
|
super(schema);
|
|
// MySQL为geometry属性默认创建索引
|
|
this.modifySchema();
|
|
}
|
|
static supportedDataTypes = [
|
|
// numeric types
|
|
"bit",
|
|
"int",
|
|
"integer",
|
|
"tinyint",
|
|
"smallint",
|
|
"mediumint",
|
|
"bigint",
|
|
"float",
|
|
"double",
|
|
"double precision",
|
|
"real",
|
|
"decimal",
|
|
"dec",
|
|
"numeric",
|
|
"fixed",
|
|
"bool",
|
|
"boolean",
|
|
// date and time types
|
|
"date",
|
|
"datetime",
|
|
"timestamp",
|
|
"time",
|
|
"year",
|
|
// string types
|
|
"char",
|
|
"nchar",
|
|
"national char",
|
|
"varchar",
|
|
"nvarchar",
|
|
"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 = [
|
|
"geometry",
|
|
"point",
|
|
"linestring",
|
|
"polygon",
|
|
"multipoint",
|
|
"multilinestring",
|
|
"multipolygon",
|
|
"geometrycollection"
|
|
];
|
|
static withLengthDataTypes = [
|
|
"char",
|
|
"varchar",
|
|
"nvarchar",
|
|
"binary",
|
|
"varbinary"
|
|
];
|
|
static withPrecisionDataTypes = [
|
|
"decimal",
|
|
"dec",
|
|
"numeric",
|
|
"fixed",
|
|
"float",
|
|
"double",
|
|
"double precision",
|
|
"real",
|
|
"time",
|
|
"datetime",
|
|
"timestamp"
|
|
];
|
|
static withScaleDataTypes = [
|
|
"decimal",
|
|
"dec",
|
|
"numeric",
|
|
"fixed",
|
|
"float",
|
|
"double",
|
|
"double precision",
|
|
"real"
|
|
];
|
|
static unsignedAndZerofillTypes = [
|
|
"int",
|
|
"integer",
|
|
"smallint",
|
|
"tinyint",
|
|
"mediumint",
|
|
"bigint",
|
|
"decimal",
|
|
"dec",
|
|
"numeric",
|
|
"fixed",
|
|
"float",
|
|
"double",
|
|
"double precision",
|
|
"real"
|
|
];
|
|
static withWidthDataTypes = [
|
|
'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;
|
|
populateDataTypeDef(type, params) {
|
|
if (MySqlTranslator.withLengthDataTypes.includes(type)) {
|
|
if (params) {
|
|
const { length } = params;
|
|
return `${type}(${length}) `;
|
|
}
|
|
else {
|
|
const { length } = MySqlTranslator.dataTypeDefaults[type];
|
|
return `${type}(${length}) `;
|
|
}
|
|
}
|
|
if (MySqlTranslator.withPrecisionDataTypes.includes(type)) {
|
|
if (params) {
|
|
const { precision, scale } = params;
|
|
if (typeof scale === 'number') {
|
|
return `${type}(${precision}, ${scale}) `;
|
|
}
|
|
return `${type}(${precision})`;
|
|
}
|
|
else {
|
|
const { precision, scale } = MySqlTranslator.dataTypeDefaults[type];
|
|
if (typeof scale === 'number') {
|
|
return `${type}(${precision}, ${scale}) `;
|
|
}
|
|
return `${type}(${precision})`;
|
|
}
|
|
}
|
|
if (MySqlTranslator.withWidthDataTypes.includes(type)) {
|
|
(0, assert_1.default)(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 'datetime';
|
|
}
|
|
if (['object', 'array'].includes(type)) {
|
|
return 'text ';
|
|
}
|
|
if (['image', 'function'].includes(type)) {
|
|
return 'text ';
|
|
}
|
|
if (type === 'ref') {
|
|
return 'char(36)';
|
|
}
|
|
return `${type} `;
|
|
}
|
|
translateAttrProjection(dataType, alias, attr) {
|
|
switch (dataType) {
|
|
case 'geometry': {
|
|
return ` st_astext(\`${alias}\`.\`${attr}\`)`;
|
|
}
|
|
default: {
|
|
return ` \`${alias}\`.\`${attr}\``;
|
|
}
|
|
}
|
|
}
|
|
translateAttrValue(dataType, value) {
|
|
if (value === null) {
|
|
return 'null';
|
|
}
|
|
switch (dataType) {
|
|
case 'geometry': {
|
|
return transformGeoData(value);
|
|
}
|
|
case 'date': {
|
|
if (value instanceof Date) {
|
|
return luxon_1.DateTime.fromJSDate(value).toFormat('yyyy-LL-dd HH:mm:ss');
|
|
}
|
|
else if (typeof value === 'number') {
|
|
return luxon_1.DateTime.fromMillis(value).toFormat('yyyy-LL-dd HH:mm:ss');
|
|
}
|
|
return value;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
translateFullTextSearch(value, entity, alias) {
|
|
const { $search } = value;
|
|
const { indexes } = this.schema[entity];
|
|
const ftIndex = indexes && indexes.find((ele) => {
|
|
const { config } = ele;
|
|
return config && config.type === 'fulltext';
|
|
});
|
|
(0, assert_1.default)(ftIndex);
|
|
const { attributes } = ftIndex;
|
|
const columns2 = attributes.map(({ name }) => `${alias}.${name}`);
|
|
return ` match(${columns2.join(',')}) against ('${$search}' in natural language mode)`;
|
|
}
|
|
translateCreateEntity(entity, options) {
|
|
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 = `create ${entityType} `;
|
|
if (storageName) {
|
|
sql += `\`${storageName}\` `;
|
|
}
|
|
else {
|
|
sql += `\`${entity}\` `;
|
|
}
|
|
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;
|
|
sql += `\`${attr}\` `;
|
|
sql += this.populateDataTypeDef(type, params);
|
|
if (notNull || type === 'geometry') {
|
|
sql += ' not null ';
|
|
}
|
|
if (unique) {
|
|
sql += ' unique ';
|
|
}
|
|
if (defaultValue !== undefined) {
|
|
(0, assert_1.default)(type !== 'ref');
|
|
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
|
|
}
|
|
if (attr === 'id') {
|
|
sql += ' primary key';
|
|
}
|
|
if (idx < Object.keys(attributes).length - 1) {
|
|
sql += ',\n';
|
|
}
|
|
});
|
|
// 翻译索引信息
|
|
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}\``;
|
|
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 += ')';
|
|
if (!replace) {
|
|
return [sql];
|
|
}
|
|
return [`drop ${entityType} \`${storageName || entity}\`;`, sql];
|
|
}
|
|
translateFnName(fnName, argumentNumber) {
|
|
switch (fnName) {
|
|
case '$add': {
|
|
return '%s + %s';
|
|
}
|
|
case '$subtract': {
|
|
return '%s - %s';
|
|
}
|
|
case '$multiply': {
|
|
return '%s * %s';
|
|
}
|
|
case '$divide': {
|
|
return '%s / %s';
|
|
}
|
|
case '$abs': {
|
|
return 'ABS(%s)';
|
|
}
|
|
case '$round': {
|
|
return 'ROUND(%s, %s)';
|
|
}
|
|
case '$ceil': {
|
|
return 'CEIL(%s)';
|
|
}
|
|
case '$floor': {
|
|
return 'FLOOR(%s)';
|
|
}
|
|
case '$pow': {
|
|
return 'POW(%s, %s)';
|
|
}
|
|
case '$gt': {
|
|
return '%s > %s';
|
|
}
|
|
case '$gte': {
|
|
return '%s >= %s';
|
|
}
|
|
case '$lt': {
|
|
return '%s < %s';
|
|
}
|
|
case '$lte': {
|
|
return '%s <= %s';
|
|
}
|
|
case '$eq': {
|
|
return '%s = %s';
|
|
}
|
|
case '$ne': {
|
|
return '%s <> %s';
|
|
}
|
|
case '$startsWith': {
|
|
return '%s like CONCAT(%s, \'%\')';
|
|
}
|
|
case '$endsWith': {
|
|
return '%s like CONCAT(\'%\', %s)';
|
|
}
|
|
case '$includes': {
|
|
return '%s like CONCAT(\'%\', %s, \'%\')';
|
|
}
|
|
case '$true': {
|
|
return '%s = true';
|
|
}
|
|
case '$false': {
|
|
return '%s = false';
|
|
}
|
|
case '$and': {
|
|
let result = '';
|
|
for (let iter = 0; iter < argumentNumber; iter++) {
|
|
result += '%s';
|
|
if (iter < argumentNumber - 1) {
|
|
result += ' and ';
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
case '$or': {
|
|
let result = '';
|
|
for (let iter = 0; iter < argumentNumber; iter++) {
|
|
result += '%s';
|
|
if (iter < argumentNumber - 1) {
|
|
result += ' or ';
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
case '$not': {
|
|
return 'not %s';
|
|
}
|
|
case '$year': {
|
|
return 'YEAR(%s)';
|
|
}
|
|
case '$month': {
|
|
return 'MONTH(%s)';
|
|
}
|
|
case '$weekday': {
|
|
return 'WEEKDAY(%s)';
|
|
}
|
|
case '$weekOfYear': {
|
|
return 'WEEKOFYEAR(%s)';
|
|
}
|
|
case '$day': {
|
|
return 'DAY(%s)';
|
|
}
|
|
case '$dayOfMonth': {
|
|
return 'DAYOFMONTH(%s)';
|
|
}
|
|
case '$dayOfWeek': {
|
|
return 'DAYOFWEEK(%s)';
|
|
}
|
|
case '$dayOfYear': {
|
|
return 'DAYOFYEAR(%s)';
|
|
}
|
|
case '$dateDiff': {
|
|
return 'DATEDIFF(%s, %s, %s)';
|
|
}
|
|
case '$contains': {
|
|
return 'ST_CONTAINS(%s, %s)';
|
|
}
|
|
case '$distance': {
|
|
return 'ST_DISTANCE(%s, %s)';
|
|
}
|
|
default: {
|
|
throw new Error(`unrecoganized function ${fnName}`);
|
|
}
|
|
}
|
|
}
|
|
translateExpression(alias, expression, refDict) {
|
|
const translateConstant = (constant) => {
|
|
if (typeof constant === 'string') {
|
|
return `'${constant}'`;
|
|
}
|
|
else if (constant instanceof Date) {
|
|
return `'${luxon_1.DateTime.fromJSDate(constant).toFormat('yyyy-LL-dd HH:mm:ss')}'`;
|
|
}
|
|
else {
|
|
(0, assert_1.default)(typeof constant === 'number');
|
|
return `${constant}`;
|
|
}
|
|
};
|
|
const translateInner = (expr) => {
|
|
const k = Object.keys(expr);
|
|
let result;
|
|
if (k.includes('#attr')) {
|
|
const attrText = `\`${alias}\`.\`${(expr)['#attr']}\``;
|
|
result = attrText;
|
|
}
|
|
else if (k.includes('#refId')) {
|
|
const refId = (expr)['#refId'];
|
|
const refAttr = (expr)['#refAttr'];
|
|
(0, assert_1.default)(refDict[refId]);
|
|
const attrText = `\`${refDict[refId]}\`.\`${refAttr}\``;
|
|
result = attrText;
|
|
}
|
|
else {
|
|
(0, assert_1.default)(k.length === 1);
|
|
if ((expr)[k[0]] instanceof Array) {
|
|
const fnName = this.translateFnName(k[0], (expr)[k[0]].length);
|
|
const args = [fnName];
|
|
args.push(...(expr)[k[0]].map((ele) => {
|
|
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
|
|
return translateConstant(ele);
|
|
}
|
|
else {
|
|
return translateInner(ele);
|
|
}
|
|
}));
|
|
result = util_1.format.apply(null, args);
|
|
}
|
|
else {
|
|
const fnName = this.translateFnName(k[0], 1);
|
|
const args = [fnName];
|
|
const arg = (expr)[k[0]];
|
|
if (['string', 'number'].includes(typeof arg) || arg instanceof Date) {
|
|
args.push(translateConstant(arg));
|
|
}
|
|
else {
|
|
args.push(translateInner(arg));
|
|
}
|
|
result = util_1.format.apply(null, args);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
return translateInner(expression);
|
|
}
|
|
populateSelectStmt(projectionText, fromText, selection, aliasDict, filterText, sorterText, indexFrom, count) {
|
|
const { hint } = selection;
|
|
// todo hint of use index
|
|
let sql = `select ${projectionText} from ${fromText}`;
|
|
if (filterText) {
|
|
sql += ` where ${filterText}`;
|
|
}
|
|
if (sorterText) {
|
|
sql += ` order by ${sorterText}`;
|
|
}
|
|
if (typeof indexFrom === 'number') {
|
|
(0, assert_1.default)(typeof count === 'number');
|
|
sql += ` limit ${indexFrom}, ${count}`;
|
|
}
|
|
if (hint?.mysql?.forUpdate) {
|
|
sql += ' for update';
|
|
}
|
|
return sql;
|
|
}
|
|
populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params) {
|
|
// todo using index
|
|
const alias = aliasDict['./'];
|
|
const now = luxon_1.DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
|
|
let sql = `update ${fromText} set ${updateText ? `${updateText},` : ''} \`${alias}\`.\`$$updateAt$$\` = '${now}'`;
|
|
if (filterText) {
|
|
sql += ` where ${filterText}`;
|
|
}
|
|
if (sorterText) {
|
|
sql += ` order by ${sorterText}`;
|
|
}
|
|
if (typeof indexFrom === 'number') {
|
|
(0, assert_1.default)(typeof count === 'number');
|
|
sql += ` limit ${indexFrom}, ${count}`;
|
|
}
|
|
return sql;
|
|
}
|
|
populateRemoveStmt(removeText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params) {
|
|
// todo using index
|
|
const alias = aliasDict['./'];
|
|
const now = luxon_1.DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
|
|
let sql = `update ${fromText} set \`${alias}\`.\`$$deleteAt$$\` = '${now}'`;
|
|
if (filterText) {
|
|
sql += ` where ${filterText}`;
|
|
}
|
|
if (sorterText) {
|
|
sql += ` order by ${sorterText}`;
|
|
}
|
|
if (typeof indexFrom === 'number') {
|
|
(0, assert_1.default)(typeof count === 'number');
|
|
sql += ` limit ${indexFrom}, ${count}`;
|
|
}
|
|
return sql;
|
|
}
|
|
}
|
|
exports.MySqlTranslator = MySqlTranslator;
|