oak-db/lib/MySQL/translator.js

1220 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MySqlTranslator = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const util_1 = require("util");
const lodash_1 = require("lodash");
const types_1 = require("oak-domain/lib/types");
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, option) {
if (option?.includedDeleted) {
return '';
}
return ` (\`${alias}\`.\`$$deleteAt$$\` is null)`;
}
makeUpSchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
// 非特殊索引自动添加 $$deleteAt$$ (by qcqcqc)
for (const index of indexes || []) {
if (index.config?.type) {
continue;
}
const indexAttrNames = index.attributes.map(attr => attr.name);
if (!indexAttrNames.includes('$$deleteAt$$')) {
index.attributes.push({ name: '$$deleteAt$$' });
}
}
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.makeUpSchema();
}
static supportedDataTypes = [
// 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 = [
"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, enumeration) {
if (['date', 'datetime', 'time', 'sequence'].includes(type)) {
return 'bigint ';
}
if (['object', 'array'].includes(type)) {
return 'json ';
}
if (['image', 'function'].includes(type)) {
return 'text ';
}
if (type === 'ref') {
return 'char(36) ';
}
if (['bool', 'boolean'].includes(type)) {
// MySQL读出来就是tinyint(1)
return 'tinyint(1) ';
}
if (type === 'money') {
return 'bigint ';
}
if (type === 'enum') {
(0, assert_1.default)(enumeration);
return `enum(${enumeration.map(ele => `'${ele}'`).join(',')}) `;
}
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 || { width: 4 };
switch (width) {
case 1: {
return 'tinyint ';
}
case 2: {
return 'smallint ';
}
case 3: {
return 'mediumint ';
}
case 4: {
return 'int ';
}
default: {
return 'bigint ';
}
}
}
return `${type} `;
}
translateAttrProjection(dataType, alias, attr) {
switch (dataType) {
case 'geometry': {
return ` st_astext(\`${alias}\`.\`${attr}\`)`;
}
default: {
return ` \`${alias}\`.\`${attr}\``;
}
}
}
translateObjectPredicate(predicate, alias, attr) {
const translateInner = (o, p) => {
let stmt2 = '';
if (o instanceof Array) {
o.forEach((ele, idx) => {
if (ele !== undefined && ele !== null) {
const part = translateInner(ele, `${p}[${idx}]`);
if (stmt2) {
stmt2 += ' and ';
}
stmt2 += `${part}`;
}
});
}
else if (typeof o === 'object') {
for (const attr2 in o) {
if (attr2 === '$and') {
o[attr2].forEach((ele) => {
const part = translateInner(ele, p);
if (stmt2) {
stmt2 += ' and ';
}
stmt2 += `${part}`;
});
}
else if (attr2 === '$or') {
let stmtOr = '';
o[attr2].forEach((ele) => {
const part = translateInner(ele, p);
if (stmtOr) {
stmtOr += ' or ';
}
stmtOr += `${part}`;
});
if (stmt2) {
stmt2 += ' and ';
}
stmt2 += `(${stmtOr})`;
}
else if (attr2 === '$contains') {
// json_contains多值的包含关系
const value = JSON.stringify(o[attr2]);
if (stmt2) {
stmt2 += ' and ';
}
if (p) {
stmt2 += `(JSON_CONTAINS(${alias}.${attr}->>"$${p}", CAST('${value}' AS JSON)))`;
}
else {
stmt2 += `(JSON_CONTAINS(${alias}.${attr}, CAST('${value}' AS JSON)))`;
}
}
else if (attr2 === '$overlaps') {
// json_overlaps多值的交叉关系
const value = JSON.stringify(o[attr2]);
if (stmt2) {
stmt2 += ' and ';
}
if (p) {
stmt2 += `(JSON_OVERLAPS(${alias}.${attr}->>"$${p}", CAST('${value}' AS JSON)))`;
}
else {
stmt2 += `(JSON_OVERLAPS(${alias}.${attr}, CAST('${value}' AS JSON)))`;
}
}
else if (attr2 === '$length') {
// json length
const length = o[attr2];
if (stmt2) {
stmt2 += ' and ';
}
if (typeof length === 'number') {
if (p) {
stmt2 += `(JSON_LENGTH(${alias}.${attr}->>"$${p}") = ${length})`;
}
else {
stmt2 += `(JSON_LENGTH(${alias}.${attr}) = ${length})`;
}
}
else {
(0, assert_1.default)(typeof length === 'object');
const op = Object.keys(length)[0];
(0, assert_1.default)(op.startsWith('$'));
if (p) {
stmt2 += `(JSON_LENGTH(${alias}.${attr}->>"$${p}") ${this.translatePredicate(op, length[op])})`;
}
else {
stmt2 += `(JSON_LENGTH(${alias}.${attr}) ${this.translatePredicate(op, length[op])})`;
}
}
}
else if (attr2.startsWith('$')) {
if (stmt2) {
stmt2 += ' and ';
}
if (p) {
stmt2 += `(${alias}.${attr}->>"$${p}" ${this.translatePredicate(attr2, o[attr2])})`;
}
else {
stmt2 += `(${alias}.${attr} ${this.translatePredicate(attr2, o[attr2])})`;
}
}
else {
// 继续子对象解构
const attr3 = attr2.startsWith('.') ? attr2.slice(1) : attr2;
const part = translateInner(o[attr2], `${p}.${attr3}`);
if (stmt2) {
stmt2 += ' and ';
}
stmt2 += `${part}`;
}
}
}
else {
// 直接的属性处理
if (stmt2) {
stmt2 += ' and ';
}
if (typeof o === 'string') {
if (p) {
stmt2 += `(${alias}.${attr}->>"$${p}" = '${o}')`;
}
else {
// 对根对象的字符串比较
stmt2 += `(${alias}.${attr} = '${o}')`;
}
}
else {
(0, assert_1.default)(p);
stmt2 += `(${alias}.${attr}->>"$${p}" = ${o})`;
}
}
return stmt2;
};
return translateInner(predicate, '');
}
translateObjectProjection(projection, alias, attr, prefix) {
let stmt = '';
const translateInner = (o, p) => {
if (o instanceof Array) {
o.forEach((item, idx) => {
const p2 = `${p}[${idx}]`;
if (typeof item === 'number') {
if (stmt) {
stmt += ', ';
}
stmt += `${alias}.${attr}->>"$${p2}"`;
stmt += prefix ? ` as \`${prefix}.${attr}${p2}\`` : ` as \`${attr}${p2}\``;
}
else if (typeof item === 'object') {
translateInner(item, p2);
}
});
}
else {
for (const key in o) {
const p2 = `${p}.${key}`;
if (typeof o[key] === 'number') {
if (stmt) {
stmt += ', ';
}
stmt += `${alias}.${attr}->>"$${p2}"`;
stmt += prefix ? ` as \`${prefix}.${attr}${p2}\`` : ` as \`${attr}${p2}\``;
}
else {
translateInner(o[key], p2);
}
}
}
};
translateInner(projection, '');
return stmt;
}
translateAttrValue(dataType, value) {
if (value === null || value === undefined) {
return 'null';
}
switch (dataType) {
case 'geometry': {
return transformGeoData(value);
}
case 'datetime':
case 'time':
case 'date': {
if (value instanceof Date) {
return `${value.valueOf()}`;
}
else if (typeof value === 'number') {
return `${value}`;
}
return `'${(new Date(value)).valueOf()}'`;
}
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)`;
}
translateAttributeDef(attr, attrDef) {
let sql = `\`${attr}\` `;
const { type, params, default: defaultValue, unique, notNull, sequenceStart, enumeration, } = attrDef;
sql += this.populateDataTypeDef(type, params, enumeration);
if (notNull || type === 'geometry') {
sql += ' not null ';
}
if (unique) {
sql += ' unique ';
}
if (typeof sequenceStart === 'number') {
sql += ' auto_increment unique ';
}
if (defaultValue !== undefined) {
(0, assert_1.default)(type !== 'ref');
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
}
if (attr === types_1.PrimaryKeyAttribute) {
sql += ' primary key';
}
return sql;
}
translateCreateEntity(entity, options) {
const ifExists = options?.ifExists || 'drop';
const { schema } = this;
const entityDef = schema[entity];
const { storageName, attributes, indexes, view, static: _static } = entityDef;
let hasSequence = false;
// todo view暂还不支持
const entityType = view ? 'view' : 'table';
let sql = `create ${entityType} `;
if (ifExists === 'omit' || (_static && ifExists === 'dropIfNotStatic')) {
sql += ' if not exists';
}
if (storageName) {
sql += `\`${storageName}\` `;
}
else {
sql += `\`${entity}\` `;
}
if (view) {
throw new Error(' view unsupported yet');
}
else {
sql += '(';
// 翻译所有的属性
Object.keys(attributes).forEach((attr, idx) => {
const attrSql = this.translateAttributeDef(attr, attributes[attr]);
if (idx !== 0) {
sql += ', ';
}
sql += attrSql;
if (typeof attributes[attr].sequenceStart === 'number') {
(0, assert_1.default)(hasSequence === false, 'Entity can only have one auto increment attribute.');
hasSequence = attributes[attr].sequenceStart;
}
});
// 翻译索引信息
if (indexes) {
sql += ',\n';
indexes.forEach(({ name, attributes, config }, idx) => {
const { unique, type, parser } = config || {};
// 因为有deleteAt的存在这里的unique没意义只能框架自己去建立checker来处理
/* 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 += '(';
attributes.forEach(({ name, size, direction }, idx2) => {
sql += `\`${name}\``;
if (size) {
sql += ` (${size})`;
}
if (direction) {
sql += ` ${direction}`;
}
if (idx2 < attributes.length - 1) {
sql += ', ';
}
});
sql += ')';
if (parser) {
sql += ` with parser ${parser}`;
}
if (idx < indexes.length - 1) {
sql += ',\n';
}
});
}
}
sql += ')';
if (typeof hasSequence === 'number') {
sql += `auto_increment = ${hasSequence}`;
}
if (ifExists === 'drop' || (!_static && ifExists === 'dropIfNotStatic')) {
return [`drop ${entityType} if exists \`${storageName || entity}\`;`, sql];
}
return [sql];
}
translateFnName(fnName, argumentNumber) {
switch (fnName) {
case '$add': {
let result = '%s';
while (--argumentNumber > 0) {
result += ' + %s';
}
return result;
}
case '$subtract': {
(0, assert_1.default)(argumentNumber === 2);
return '%s - %s';
}
case '$multiply': {
let result = '%s';
while (--argumentNumber > 0) {
result += ' * %s';
}
return result;
}
case '$divide': {
(0, assert_1.default)(argumentNumber === 2);
return '%s / %s';
}
case '$abs': {
return 'ABS(%s)';
}
case '$round': {
(0, assert_1.default)(argumentNumber === 2);
return 'ROUND(%s, %s)';
}
case '$ceil': {
return 'CEIL(%s)';
}
case '$floor': {
return 'FLOOR(%s)';
}
case '$mod': {
return 'MOD(%s, %s)';
}
case '$pow': {
(0, assert_1.default)(argumentNumber === 2);
return 'POW(%s, %s)';
}
case '$gt': {
(0, assert_1.default)(argumentNumber === 2);
return '%s > %s';
}
case '$gte': {
(0, assert_1.default)(argumentNumber === 2);
return '%s >= %s';
}
case '$lt': {
(0, assert_1.default)(argumentNumber === 2);
return '%s < %s';
}
case '$lte': {
return '%s <= %s';
}
case '$eq': {
(0, assert_1.default)(argumentNumber === 2);
return '%s = %s';
}
case '$ne': {
(0, assert_1.default)(argumentNumber === 2);
return '%s <> %s';
}
case '$startsWith': {
(0, assert_1.default)(argumentNumber === 2);
return '%s like CONCAT(%s, \'%\')';
}
case '$endsWith': {
(0, assert_1.default)(argumentNumber === 2);
return '%s like CONCAT(\'%\', %s)';
}
case '$includes': {
(0, assert_1.default)(argumentNumber === 2);
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)';
}
// 这个实现有问题DATEDIFF只是计算两个日期之间的天数差只接受两个参数放在translateExperession里实现
// case '$dateDiff': {
// assert(argumentNumber === 3);
// return 'DATEDIFF(%s, %s, %s)';
// }
case '$contains': {
(0, assert_1.default)(argumentNumber === 2);
return 'ST_CONTAINS(%s, %s)';
}
case '$distance': {
(0, assert_1.default)(argumentNumber === 2);
return 'ST_DISTANCE(%s, %s)';
}
case '$concat': {
let result = ' concat(%s';
while (--argumentNumber > 0) {
result += ', %s';
}
result += ')';
return result;
}
// ========== 聚合函数 ==========
case '$$count': {
return 'COUNT(%s)';
}
case '$$sum': {
return 'SUM(%s)';
}
case '$$max': {
return 'MAX(%s)';
}
case '$$min': {
return 'MIN(%s)';
}
case '$$avg': {
return 'AVG(%s)';
}
default: {
throw new Error(`unrecoganized function ${fnName}`);
}
}
}
translateAttrInExpression(entity, attr, exprText) {
const { attributes } = this.schema[entity];
const { type } = attributes[attr];
if (['date', 'time', 'datetime'].includes(type)) {
// 从unix时间戵转成date类型参加expr的运算
return `from_unixtime(${exprText} / 1000)`;
}
return exprText;
}
translateExpression(entity, alias, expression, refDict) {
const translateConstant = (constant) => {
if (constant instanceof Date) {
return ` from_unixtime(${constant.valueOf()}/1000)`;
}
else if (typeof constant === 'string') {
return ` '${constant}'`;
}
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 = this.translateAttrInExpression(entity, (expr)['#attr'], attrText);
}
else if (k.includes('#refId')) {
const refId = (expr)['#refId'];
const refAttr = (expr)['#refAttr'];
(0, assert_1.default)(refDict[refId]);
const [refAlias, refEntity] = refDict[refId];
const attrText = `\`${refAlias}\`.\`${refAttr}\``;
// 这里必须使用refEntity否则在filter深层嵌套节点表达式时会出现entity不对应
result = this.translateAttrInExpression(refEntity, (expr)['#refAttr'], attrText);
}
else {
(0, assert_1.default)(k.length === 1);
const fnKey = k[0];
const fnArgs = (expr)[fnKey];
// 特殊处理日期相关函数
if (fnKey === '$dateDiff') {
// $dateDiff: [date1, date2, unit]
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 3);
const [date1Expr, date2Expr, unit] = fnArgs;
// 转换日期表达式
const translateDateArg = (arg) => {
if (arg instanceof Date) {
return `FROM_UNIXTIME(${arg.valueOf()} / 1000)`;
}
else if (typeof arg === 'number') {
return `FROM_UNIXTIME(${arg} / 1000)`;
}
else {
return translateInner(arg);
}
};
const date1Str = translateDateArg(date1Expr);
const date2Str = translateDateArg(date2Expr);
// MySQL TIMESTAMPDIFF 单位映射
const unitMap = {
's': 'SECOND',
'm': 'MINUTE',
'h': 'HOUR',
'd': 'DAY',
'M': 'MONTH',
'y': 'YEAR'
};
const mysqlUnit = unitMap[unit];
if (!mysqlUnit) {
throw new Error(`Unsupported date diff unit: ${unit}`);
}
// TIMESTAMPDIFF(unit, date1, date2) 返回 date2 - date1
// 但类型定义是 date1 - date2所以参数顺序要反过来
result = `TIMESTAMPDIFF(${mysqlUnit}, ${date2Str}, ${date1Str})`;
}
else if (fnKey === '$dateCeil') {
// $dateCeil: [date, unit]
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 2);
const [dateExpr, unit] = fnArgs;
const getTimestampExpr = (arg) => {
if (arg instanceof Date) {
return `${arg.valueOf()}`;
}
else if (typeof arg === 'number') {
return `${arg}`;
}
else {
const k = Object.keys(arg);
if (k.includes('#attr')) {
return `\`${alias}\`.\`${arg['#attr']}\``;
}
else if (k.includes('#refId')) {
const refId = arg['#refId'];
const refAttr = arg['#refAttr'];
return `\`${refDict[refId][0]}\`.\`${refAttr}\``;
}
return translateInner(arg);
}
};
const tsExpr = getTimestampExpr(dateExpr);
const msPerUnit = {
's': 1000,
'm': 60000,
'h': 3600000,
'd': 86400000,
};
if (msPerUnit[unit]) {
// CEIL 向上取整
result = `CEIL(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
}
else {
(0, assert_1.default)(typeof unit === 'string', 'unit should be string');
console.warn('暂不支持 $dateCeil 对月年单位的处理');
throw new Error(`Unsupported date ceil unit: ${unit}`);
}
}
else if (fnKey === '$dateFloor') {
// $dateFloor: [date, unit]
(0, assert_1.default)(fnArgs instanceof Array && fnArgs.length === 2);
const [dateExpr, unit] = fnArgs;
// 获取毫秒时间戳表达式
const getTimestampExpr = (arg) => {
if (arg instanceof Date) {
return `${arg.valueOf()}`;
}
else if (typeof arg === 'number') {
return `${arg}`;
}
else {
// 属性引用,直接返回属性(存储本身就是毫秒时间戳)
const k = Object.keys(arg);
if (k.includes('#attr')) {
return `\`${alias}\`.\`${arg['#attr']}\``;
}
else if (k.includes('#refId')) {
const refId = arg['#refId'];
const refAttr = arg['#refAttr'];
return `\`${refDict[refId][0]}\`.\`${refAttr}\``;
}
// 其他表达式递归处理
return translateInner(arg);
}
};
const tsExpr = getTimestampExpr(dateExpr);
// 固定间隔单位:直接用时间戳数学运算
const msPerUnit = {
's': 1000,
'm': 60000,
'h': 3600000,
'd': 86400000,
};
if (msPerUnit[unit]) {
// FLOOR(timestamp / interval) * interval
result = `FLOOR(${tsExpr} / ${msPerUnit[unit]}) * ${msPerUnit[unit]}`;
}
else {
(0, assert_1.default)(typeof unit === 'string', 'unit should be string');
console.warn('暂不支持 $dateFloor 对月年单位的处理');
throw new Error(`Unsupported date floor unit: ${unit}`);
}
}
else if (fnArgs instanceof Array) {
// 原有的数组参数处理逻辑
const fnName = this.translateFnName(fnKey, fnArgs.length);
const args = [fnName];
args.push(...fnArgs.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(fnKey, 1);
const args = [fnName];
const arg = fnArgs;
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, aliasDict, filterText, sorterText, groupByText, indexFrom, count, option) {
// todo hint of use index
let sql = `select ${projectionText} from ${fromText}`;
if (filterText) {
sql += ` where ${filterText}`;
}
if (sorterText) {
sql += ` order by ${sorterText}`;
}
if (groupByText) {
sql += ` group by ${groupByText}`;
}
if (typeof indexFrom === 'number') {
(0, assert_1.default)(typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
if (option?.forUpdate) {
sql += ' for update';
if (typeof option?.forUpdate === 'string') {
sql += ` ${option.forUpdate}`;
}
}
return sql;
}
populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, option) {
// todo using index
(0, assert_1.default)(updateText);
let sql = `update ${fromText} set ${updateText}`;
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(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, option) {
// todo using index
const alias = aliasDict['./'];
if (option?.deletePhysically) {
// assert(!updateText, 'physically delete does not support setting trigger data');
let sql = `delete ${alias} 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}`;
}
return sql;
}
// 新的remove应该包含$$deleteAt$$的值了
/* const now = Date.now();
const updateText2 = updateText ? `${updateText}, \`${alias}\`.\`$$deleteAt$$\` = '${now}'` : `\`${alias}\`.\`$$deleteAt$$\` = '${now}'`; */
(0, assert_1.default)(updateText.includes(types_1.DeleteAtAttribute));
let sql = `update ${fromText} set ${updateText}`;
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;
}
/**
* 将MySQL返回的Type回译成oak的类型是 populateDataTypeDef 的反函数
* @param type
*/
reTranslateToAttribute(type) {
const withLengthDataTypes = MySqlTranslator.withLengthDataTypes.join('|');
let result = (new RegExp(`^(${withLengthDataTypes})\\((\\d+)\\)$`)).exec(type);
if (result) {
return {
type: result[1],
params: {
length: parseInt(result[2]),
}
};
}
const withPrecisionDataTypes = MySqlTranslator.withPrecisionDataTypes.join('|');
result = (new RegExp(`^(${withPrecisionDataTypes})\\((\\d+),(d+)\\)$`)).exec(type);
if (result) {
return {
type: result[1],
params: {
precision: parseInt(result[2]),
scale: parseInt(result[3]),
},
};
}
result = (/^enum\((\S+)\)$/).exec(type);
if (result) {
const enumeration = result[1].split(',').map(ele => ele.slice(1, -1));
return {
type: 'enum',
enumeration
};
}
return {
type: type,
};
}
// 分析当前数据库结构图
async readSchema(execFn) {
const result = {};
const sql = 'show tables;';
const [tables] = await execFn(sql);
for (const tableItem of tables) {
const table = Object.values(tableItem)[0];
const [tableResult] = await execFn(`desc \`${table}\``);
const attributes = {};
for (const attrItem of tableResult) {
const { Field: attrName, Null: isNull, Type: type, Key: key } = attrItem;
attributes[attrName] = {
...this.reTranslateToAttribute(type),
notNull: isNull.toUpperCase() === 'NO',
unique: key.toUpperCase() === 'UNI',
};
// 自增列只可能是seq
if (attrName === '$$seq$$') {
attributes[attrName].sequenceStart = 10000;
}
}
Object.assign(result, {
[table]: {
attributes,
}
});
const [indexedColumns] = (await execFn(`show index from \`${table}\``));
if (indexedColumns.length) {
const groupedColumns = (0, lodash_1.groupBy)(indexedColumns.sort((ele1, ele2) => ele1.Key_name.localeCompare(ele2.Key_name) || ele1.Seq_in_index - ele2.Seq_in_index), 'Key_name');
const indexes = Object.values(groupedColumns).map((ele) => {
const index = {
name: ele[0].Key_name,
attributes: ele.map(ele2 => ({
name: ele2.Column_name,
direction: ele2.Collation === 'D' ? 'DESC' : (ele2.Collation === 'A' ? 'ASC' : undefined),
size: ele2.Sub_part || undefined,
})),
};
if (ele[0].Non_unique === 0 || ele[0].Index_type.toUpperCase() !== 'BTREE') {
index.config = {};
if (ele[0].Non_unique === 0) {
index.config.unique = true;
}
if (ele[0].Index_type.toUpperCase() !== 'BTREE') {
index.config.type = ele[0].Index_type.toLowerCase();
}
}
return index;
});
Object.assign(result[table], {
indexes,
});
}
}
return result;
}
}
exports.MySqlTranslator = MySqlTranslator;