jql2sql
Version:
Transpiling JQL to SQL
156 lines (133 loc) • 4.33 kB
JavaScript
const { has } = require("lodash");
const { KINDS, isNumeric, clearAST } = require("./utils");
/**
* b or "b" -> 'b'
* @param {*} v b or "b"
* @returns 'b'
*/
function valueForIn(v) {
v = v.trim();
if (v[0] == '\"')
v = v.substr(1, v.length - (1*2));
// v is not number, we surround with '
if (!isNumeric(v)) {
v = '\'' + v+ '\''
}
return v;
}
function transpileFOV(ast, UIField2Column) {
const JQLOps2JSQLOps = {
/** We may use these operators without transfiling */
'=': '=',
'!=': '!=',
'>': '>',
'<': '<',
'>=': '>=',
'<=': '<=',
'in': 'in',
'not in': 'not in',
/** Operators should be transpiled */
'~': 'LIKE',
'!~': 'NOT LIKE',
'is': '=',
'is not': '!='
}
// console.log(ast);
const UIfield = ast.cleaned.field;
const column = (UIField2Column.has(UIfield))? UIField2Column.get(UIfield): UIfield;
const JQLOperator = ast.cleaned.operator;
const SQLOperator = JQLOps2JSQLOps[JQLOperator];
let value = ast.cleaned.value;
// console.log('transpileFOV() ' + `${field} ${operator} ${value}`);
if (JQLOperator === 'in' || JQLOperator === 'not in') {
value = value.substr(1, value.length - (1*2));
if (value.indexOf(',') >= 0) {
let vs = value.split(',');
vs = vs.map(v => valueForIn(v));
value = vs.join(', ');
}
else {
value = valueForIn(value);
}
value = '(' + value + ')';
}
if (JQLOperator === '~'
|| JQLOperator === '!~') {
if (has(ast, 'valueHint') && has(ast.valueHint, 'text')) {
const hintText = ast.valueHint.text;
if (hintText == 'noQuoteValueNoSpace') {
value = `'${value}'`;
}
else if (hintText == 'doubleQuoteValueNoSpace') {
// Escape " i.e) "blah" -> blah
value = value.substr(1, value.length - (1*2));
value = `'${value}'`;
}
else if (hintText == 'doubleQuoteValueWithSpace') {
// Escape " i.e) "blah" -> blah
value = value.substr(1, value.length - (1*2));
// value contains ' '(space)
if (value.indexOf(' ') >= 0) {
const vs = value.split(' ');
const fovs = vs.map(v => `${column} ${SQLOperator} '${v}'`);
value = '(' + fovs.join(' or ') + ')';
}
return value;
}
else if (hintText == 'doubleQuoteValueWithLeftAsterisk'
|| hintText == 'doubleQuoteValueWithRightAsterisk'
|| hintText == 'doubleQuoteValueWithBothAsterisk') {
// Escape "*blah" i.e) "*blah" -> *blah
// Escape "blah*" i.e) "blah*" -> blah*
// Escape "*blah*" i.e) "*blah*" -> *blah*
value = value.substr(1, value.length - 2);
value = value.replace('*', '%');
value = value.replace('*', '%');
value = `'${value}'`;
}
else if (hintText == 'nestedDoubleQuoteValue') {
// Escape "\"blah\"" i.e) "\"blah\"" -> blah
value = value.substr(3, value.length - (3*2));
value = `'%${value}%'`;
}
}
}
return `${column} ${SQLOperator} ${value}`;
}
/**
* Transpile AST to SQL where clause
*
* @param {*} ast Abstrat syntax tree
* @param {*} UIField2Column Replacing UI field name(in AST) with table's column name(in DB)
* @returns SQL where clause
*/
function transpileAST(ast, UIField2Column) {
let where = '';
if (ast.kinds == KINDS.AST_LTRT) {
let lt = transpileAST(clearAST(ast.expLt), UIField2Column);
let rt = transpileAST(clearAST(ast.expRt), UIField2Column);
where = `${lt} ${ast.andOr} ${rt}`;
}
else if (ast.kinds == KINDS.AST_FOV) {
where = transpileFOV(clearAST(ast), UIField2Column);
}
else if (ast.kinds == KINDS.AST_BRACKET) {
where = '(' + transpileAST(clearAST(ast.exp), UIField2Column) + ')';
}
return where;
}
/**
* ASTs are trasnfiled to SQL where clause
*
* @param {*} ast 'parser.results' should be given
* @param {*} UIField2Column Replacing UI field name(in AST) with table's column name(in DB)
* @returns SQL where clause
*/
function transpile2SQL(ast, UIField2Column) {
if (UIField2Column === undefined || UIField2Column === null) {
UIField2Column = new Map();
}
let where = transpileAST(clearAST(ast), UIField2Column);
return where.trim();
}
module.exports = { transpile2SQL };