UNPKG

pg-slang

Version:

Convert informal SQL SELECT to formal SQL.

213 lines (188 loc) 7.48 kB
const Parser = require('flora-sql-parser').Parser; const astToSQL = require('flora-sql-parser').util.astToSQL; const NULL = /^null$/i; const BOOL = /^(true|false)$/i; const NUMBER = /^[\d\.\-e]$/i; const STRING = /^\'[^\']\'$/; const IDENTIFIER = /^\*$|^\w+$|^\"[^\"]*\"$/; const HINT_ALL = /^(all|each|every):/i; const HINT_SUM = /^(sum|gross|total|whole|aggregate):/i; const HINT_AVG = /^(avg|mid|par|mean|norm|center|centre|average|midpoint):/i; const FROMT = [{table: 't', as: null}]; function number(value) { return {type: 'number', value}; }; function column(column) { return {type: 'column_ref', table: null, column}; }; function table(table, as=null) { return {db: null, table, as}; }; function binaryExpr(operator, left, right, parentheses=false) { return {type: 'binary_expr', operator, left, right, parentheses}; }; function dequote(txt) { return /^[\'\"]/.test(txt)? txt.slice(1, -1):txt; }; function clean(txt) { txt = txt.replace(/\/\*.*?\*\//gm, ''); txt = txt.replace(/\-\-.*/g, '').trim(); return txt.endsWith(';')? txt.slice(0, -1):txt; }; function setLimit(ast, max) { var value = Math.min(ast.limit? ast.limit[1].value:max, max); ast.limit = [{type: 'number', value}]; }; function getSum(cols) { if(cols.length===0) return [number(0)]; if(cols.length===1) return [cols[0]]; var asw = binaryExpr('+', cols[0], cols[1], true); for(var i=2, I=cols.length; i<I; i++) asw.right = binaryExpr('+', asw.right, cols[i]); return [asw]; }; function getAvg(cols) { if(cols.length===0) return [number(0)]; var asw = binaryExpr('/', getSum(cols)[0], number(cols.length), true); return [asw]; }; function parseExpression(exp) { exp = exp.replace(/<>/g, '!=').replace(/@@/g, '<>'); var txt = `SELECT * FROM T WHERE (${exp})`; var asw = new Parser().parse(txt); return asw.where; }; function parseValue(val) { if(NULL.test(val)) return {type: 'null', value: null}; if(BOOL.test(val)) return {type: 'bool', value: /true/i.test(val)}; if(NUMBER.test(val)) return {type: 'number', value: parseFloat(val)}; if(STRING.test(val)) return {type: 'string', value: val.slice(1, -1)}; if(IDENTIFIER.test(val)) return column(dequote(val)); return parseExpression(val); }; async function getColumn(type, from, txt, fn, ths=null) { var hint = null; if(HINT_ALL.test(txt)) hint = 'all'; else if(HINT_SUM.test(txt)) hint = 'sum'; else if(HINT_AVG.test(txt)) hint = 'avg'; var ans = await fn.call(ths, hint? txt.replace(/.*?:/, ''):txt, type, hint, from); ans = (ans||[]).map(val => parseValue(val)); if(hint==null || hint==='all') return ans; return hint==='sum'? getSum(ans):getAvg(ans); }; function setSubexpression(type, from, ast, k, fn, ths=null) { if(ast[k]==null || typeof ast[k]!=='object') return Promise.resolve(); if(ast[k].type==='column_ref') return getColumn(type, from, ast[k].column, fn, ths).then(ans => ast[k]=ans[0]); return Promise.all(Object.keys(ast[k]).map(l => setSubexpression(type, from, ast[k], l, fn, ths))); }; function getExpression(type, from, ast, fn, ths=null) { if(ast.type==='column_ref') return getColumn(type, from, ast.column, fn, ths); return Promise.all(Object.keys(ast).map(k => setSubexpression(type, from, ast, k, fn, ths))).then(() => [ast]); }; function asExpression(expr) { var sql = astToSQL({type: 'select', from: FROMT, columns: [{expr, as: null}]}); return sql.substring(7, sql.length-9).replace(/([\'\"])/g, '$1$1'); }; function asColumn(col, len, as) { return txt = len>1 && as!=null? as+': '+col:as; }; async function tweakColumns(from, ast, fn, ths=null) { var columns = ast.columns, to = []; var ans = await Promise.all(columns.map(col => getExpression('columns', from, col.expr, fn, ths))); for(var i=0, I=columns.length; i<I; i++) { var col = columns[i], exps = ans[i]; for(var exp of exps) { if(exp.type!=='column_ref') to.push({expr: exp, as: col.as==null? asExpression(exp):col.as}); else to.push({expr: exp, as: asColumn(exp.column, exps.length, col.as)}); } } ast.columns = to; }; async function tweakWhere(from, ast, fn, ths=null) { await setSubexpression('where', from, ast, 'where', fn, ths); }; async function tweakHaving(from, ast, fn, ths=null) { await setSubexpression('having', from, ast, 'having', fn, ths); }; async function tweakOrderBy(from, ast, fn, ths=null) { var orderby = ast.orderby, to = []; var ans = await Promise.all(orderby.map(col => getExpression('orderBy', from, col.expr, fn, ths))); for(var i=0, I=orderby.length; i<I; i++) { var col = orderby[i], exps = ans[i]; for(var exp of exps) to.push({expr: exp, type: col.type}); } ast.orderby = to; }; async function tweakGroupBy(from, ast, fn, ths=null) { var groupby = ast.groupby, to = []; var ans = await Promise.all(groupby.map(exp => getExpression('groupBy', exp, fn, ths))); for(var val of ans) to.push.apply(to, val); ast.groupby = to }; function forkWhere(ast) { var txt = `SELECT * FROM T WHERE TRUE AND TRUE`; var asw = new Parser().parse(txt); if(ast.where) { asw.where.left = ast.where; ast.where = asw.where; ast.where.left.parentheses = true; } else ast.where = asw.where; return ast; }; function appendWhere(ast, exp) { exp = exp.replace(/<>/g, '!=').replace(/@@/g, '<>'); var txt = `SELECT * FROM T WHERE FALSE OR (${exp})`; var asw = new Parser().parse(txt); var opr = asw.where.right.operator.replace(/<>/, '@@'); asw.where.right.operator = opr; if(ast.where.right.value===true) { ast.where.right = asw.where; ast.where.right.parentheses = true; } else { asw.where.left = ast.where.right.right; ast.where.right.right = asw.where; } return ast; }; async function scanFrom(ast, fn, ths=null) { var from = ast.from, to = new Set(), where = []; var ans = await Promise.all(from.map(b => fn.call(ths, b.table, 'from', null, null)||[])); for(var vals of ans) { for(var val of vals) { if(IDENTIFIER.test(val)) to.add(dequote(val)); else where.push(val); } } return {from: Array.from(to), where}; }; function tweakFrom(ast, scn) { var ast = forkWhere(ast); for(var val of scn.where) appendWhere(ast, val); ast.from = scn.from.map(v => table(v)); }; function slang(txt, fn, ths=null, opt={}) { var ast = new Parser().parse(clean(txt)), rdy = []; if(ast.type!=='select') return Promise.reject(new Error(`Only SELECT supported <<${txt}>>.`)); return scanFrom(ast, fn, ths).then(scn => { var from = scn.from; if(from.length===0 && opt.from!=null) from.push(opt.from); if(typeof ast.columns!=='string') rdy.push(tweakColumns(from, ast, fn, ths)); if(ast.where!=null) rdy.push(tweakWhere(from, ast, fn, ths)); if(ast.having!=null) rdy.push(tweakHaving(from, ast, fn, ths)); if(ast.orderby!=null) rdy.push(tweakOrderBy(from, ast, fn, ths)); if(ast.groupby!=null) rdy.push(tweakGroupBy(from, ast, fn, ths)); return Promise.all(rdy).then(() => scn); }).then(scn => { tweakFrom(ast, scn); if(ast.from.length===0) ast.from.push(table('null')); var lim = opt.limits? opt.limits[ast.from[0].table]||0:opt.limit||0; if(lim) setLimit(ast, lim); return astToSQL(ast); }); }; module.exports = slang;