UNPKG

odata-v4-sql

Version:
412 lines (368 loc) 12.4 kB
import { Token } from "odata-v4-parser/lib/lexer"; import { Literal } from "odata-v4-literal"; import { SqlOptions } from "./index"; export class SQLLiteral extends Literal{ static convert(type:string, value:string):any { return (new SQLLiteral(type, value)).valueOf(); } 'Edm.String'(value:string){ return "'" + decodeURIComponent(value).slice(1, -1).replace(/''/g, "'") + "'"; } 'Edm.Guid'(value:string){ return "'" + decodeURIComponent(value) + "'"; } 'Edm.Date'(value:string){ return "'" + value + "'"; } 'Edm.DateTimeOffset'(value:string):any{ return "'" + value.replace("T", " ").replace("Z", " ").trim() + "'"; } 'Edm.Boolean'(value:string):any{ value = value || ''; switch (value.toLowerCase()){ case 'true': return 1; case 'false': return 0; default: return "NULL"; } } 'null'(value:string){ return "NULL"; } } export enum SQLLang{ ANSI, MsSql, MySql, PostgreSql, Oracle } export class Visitor{ protected options:SqlOptions type:SQLLang; select:string = ""; where:string = ""; orderby:string = ""; skip:number limit:number inlinecount:boolean navigationProperty:string includes:Visitor[] = []; parameters:any = new Map(); protected parameterSeed:number = 0; protected originalWhere:string; ast:Token constructor(options = <SqlOptions>{}){ this.options = options; if (this.options.useParameters != false) this.options.useParameters = true; this.type = options.type || SQLLang.ANSI; } from(table:string){ let sql = `SELECT ${this.select} FROM [${table}] WHERE ${this.where} ORDER BY ${this.orderby}`; switch (this.type){ case SQLLang.Oracle: case SQLLang.MsSql: if (typeof this.skip == "number") sql += ` OFFSET ${this.skip} ROWS`; if (typeof this.limit == "number"){ if (typeof this.skip != "number") sql += " OFFSET 0 ROWS"; sql += ` FETCH NEXT ${this.limit} ROWS ONLY`; } break; case SQLLang.MySql: case SQLLang.PostgreSql: default: if (typeof this.limit == "number") sql += ` LIMIT ${this.limit}`; if (typeof this.skip == "number") sql += ` OFFSET ${this.skip}`; break; } return sql; } asMsSql(){ this.type = SQLLang.MsSql; let rx = new RegExp("\\?", "g"); let keys = this.parameters.keys(); this.originalWhere = this.where; this.where = this.where.replace(rx, () => `@${keys.next().value}`); this.includes.forEach((item) => item.asMsSql()); return this; } asOracleSql(){ this.type = SQLLang.Oracle; let rx = new RegExp("\\?", "g"); let keys = this.parameters.keys(); this.originalWhere = this.where; this.where = this.where.replace(rx, () => `:${keys.next().value}`); this.includes.forEach((item) => item.asOracleSql()); return this; } asAnsiSql(){ this.type = SQLLang.ANSI; this.where = this.originalWhere || this.where; this.includes.forEach((item) => item.asAnsiSql()); return this; } asType(){ switch (this.type){ case SQLLang.MsSql: return this.asMsSql(); case SQLLang.ANSI: case SQLLang.MySql: case SQLLang.PostgreSql: return this.asAnsiSql(); case SQLLang.Oracle: return this.asOracleSql(); default: return this; } } Visit(node:Token, context?:any){ this.ast = this.ast || node; context = context || { target: "where" }; if (node){ var visitor = this[`Visit${node.type}`]; if (visitor) visitor.call(this, node, context); else console.log(`Unhandled node type: ${node.type}`, node); } if (node == this.ast){ if (!this.select) this.select = `*`; if (!this.where) this.where = "1 = 1"; if (!this.orderby) this.orderby = "1"; } return this; } protected VisitODataUri(node:Token, context:any){ this.Visit(node.value.resource, context); this.Visit(node.value.query, context); } protected VisitExpand(node: Token, context: any) { node.value.items.forEach((item) => { let expandPath = item.value.path.raw; let visitor = this.includes.filter(v => v.navigationProperty == expandPath)[0]; if (!visitor){ visitor = new Visitor(this.options); visitor.parameterSeed = this.parameterSeed; this.includes.push(visitor); } visitor.Visit(item); this.parameterSeed = visitor.parameterSeed; }); } protected VisitExpandItem(node: Token, context: any) { this.Visit(node.value.path, context); if (node.value.options) node.value.options.forEach((item) => this.Visit(item, context)); } protected VisitExpandPath(node: Token, context: any) { this.navigationProperty = node.raw; } protected VisitQueryOptions(node:Token, context:any){ node.value.options.forEach((option) => this.Visit(option, context)); } protected VisitInlineCount(node:Token, context:any){ this.inlinecount = Literal.convert(node.value.value, node.value.raw); } protected VisitFilter(node:Token, context:any){ context.target = "where"; this.Visit(node.value, context); if (!this.where) this.where = "1 = 1"; } protected VisitOrderBy(node:Token, context:any){ context.target = "orderby"; node.value.items.forEach((item, i) => { this.Visit(item, context); if (i < node.value.items.length - 1) this.orderby += ", "; }); } protected VisitOrderByItem(node:Token, context:any){ this.Visit(node.value.expr, context); this.orderby += node.value.direction > 0 ? " ASC" : " DESC"; } protected VisitSkip(node:Token, context:any){ this.skip = +node.value.raw; } protected VisitTop(node:Token, context:any){ this.limit = +node.value.raw; } protected VisitSelect(node:Token, context:any){ context.target = "select"; node.value.items.forEach((item, i) => { this.Visit(item, context); if (i < node.value.items.length - 1) this.select += ", "; }); } protected VisitSelectItem(node:Token, context:any){ let item = node.raw.replace(/\//g, '.'); this.select += `[${item}]`; } protected VisitAndExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " AND "; this.Visit(node.value.right, context); } protected VisitOrExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " OR "; this.Visit(node.value.right, context); } protected VisitBoolParenExpression(node:Token, context:any){ this.where += "("; this.Visit(node.value, context); this.where += ")"; } protected VisitCommonExpression(node:Token, context:any){ this.Visit(node.value, context); } protected VisitFirstMemberExpression(node:Token, context:any){ this.Visit(node.value, context); } protected VisitMemberExpression(node:Token, context:any){ this.Visit(node.value, context); } protected VisitPropertyPathExpression(node:Token, context:any){ if (node.value.current && node.value.next){ this.Visit(node.value.current, context); context.identifier += "."; this.Visit(node.value.next, context); }else this.Visit(node.value, context); } protected VisitSingleNavigationExpression(node:Token, context:any){ if (node.value.current && node.value.next){ this.Visit(node.value.current, context); this.Visit(node.value.next, context); }else this.Visit(node.value, context); } protected VisitODataIdentifier(node:Token, context:any){ this[context.target] += `[${node.value.name}]`; context.identifier = node.value.name; } protected VisitEqualsExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " = "; this.Visit(node.value.right, context); if (this.options.useParameters && context.literal == null){ this.where = this.where.replace(/= \?$/, "IS NULL").replace(new RegExp(`\\? = \\[${context.identifier}\\]$`), `[${context.identifier}] IS NULL`); }else if (context.literal == "NULL"){ this.where = this.where.replace(/= NULL$/, "IS NULL").replace(new RegExp(`NULL = \\[${context.identifier}\\]$`), `[${context.identifier}] IS NULL`); } } protected VisitNotEqualsExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " <> "; this.Visit(node.value.right, context); if (this.options.useParameters && context.literal == null){ this.where = this.where.replace(/<> \?$/, "IS NOT NULL").replace(new RegExp(`\\? <> \\[${context.identifier}\\]$`), `[${context.identifier}] IS NOT NULL`); }else if (context.literal == "NULL"){ this.where = this.where.replace(/<> NULL$/, "IS NOT NULL").replace(new RegExp(`NULL <> \\[${context.identifier}\\]$`), `[${context.identifier}] IS NOT NULL`); } } protected VisitLesserThanExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " < "; this.Visit(node.value.right, context); } protected VisitLesserOrEqualsExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " <= "; this.Visit(node.value.right, context); } protected VisitGreaterThanExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " > "; this.Visit(node.value.right, context); } protected VisitGreaterOrEqualsExpression(node:Token, context:any){ this.Visit(node.value.left, context); this.where += " >= "; this.Visit(node.value.right, context); } protected VisitLiteral(node:Token, context:any){ if (this.options.useParameters){ let name = `p${this.parameterSeed++}`; let value = Literal.convert(node.value, node.raw); context.literal = value; this.parameters.set(name, value); this.where += "?"; }else this.where += (context.literal = SQLLiteral.convert(node.value, node.raw)); } protected VisitMethodCallExpression(node:Token, context:any){ var method = node.value.method; var params = node.value.parameters || []; switch (method){ case "contains": this.Visit(params[0], context); if (this.options.useParameters){ let name = `p${this.parameterSeed++}`; let value = Literal.convert(params[1].value, params[1].raw); this.parameters.set(name, `%${value}%`); this.where += " like ?"; }else this.where += ` like '%${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}%'`; break; case "endswith": this.Visit(params[0], context); if (this.options.useParameters){ let name = `p${this.parameterSeed++}`; let value = Literal.convert(params[1].value, params[1].raw); this.parameters.set(name, `%${value}`); this.where += " like ?"; }else this.where += ` like '%${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}'`; break; case "startswith": this.Visit(params[0], context); if (this.options.useParameters){ let name = `p${this.parameterSeed++}`; let value = Literal.convert(params[1].value, params[1].raw); this.parameters.set(name, `${value}%`); this.where += " like ?"; }else this.where += ` like '${SQLLiteral.convert(params[1].value, params[1].raw).slice(1, -1)}%'`; break; case "indexof": let fn = ""; switch (this.type) { case SQLLang.MsSql: fn = "CHARINDEX"; break; case SQLLang.ANSI: case SQLLang.MySql: case SQLLang.PostgreSql: default: fn = "INSTR"; break; } if (fn === "CHARINDEX"){ const tmp = params[0]; params[0] = params[1]; params[1] = tmp; } this.where += `${fn}(`; this.Visit(params[0], context); this.where += ', '; this.Visit(params[1], context); this.where += ") - 1"; break; case "round": this.where += "ROUND("; this.Visit(params[0], context); this.where += ")"; break; case "length": this.where += "LEN("; this.Visit(params[0], context); this.where += ")"; break; case "tolower": this.where += "LCASE("; this.Visit(params[0], context); this.where += ")"; break; case "toupper": this.where += "UCASE("; this.Visit(params[0], context); this.where += ")"; break; case "floor": case "ceiling": case "year": case "month": case "day": case "hour": case "minute": case "second": this.where += `${method.toUpperCase()}(`; this.Visit(params[0], context); this.where += ")"; break; case "now": this.where += "NOW()"; break; case "trim": this.where += "TRIM(' ' FROM "; this.Visit(params[0], context); this.where += ")"; break; } } }