/* * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre * Departamento de Informatica - Universidade Federal do Parana * * This file is part of blend. * * blend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * blend is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with blend. If not, see <http://www.gnu.org/licenses/>. */ import { Adapter } from "../core/adapter"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; import { Clause } from "../core/clause"; import { Filter, FilterOperator } from "../core/filter"; import { AggregationType, RelationType } from "../common/types"; import { View } from "../core/view"; import { Pool, PoolConfig } from "pg"; interface DimInfo { dim: Dimension; views: View[]; } export class PostgresAdapter extends Adapter { private pool: Pool; constructor (config: PoolConfig) { super(); this.pool = new Pool(config); } public getDataFromView(view: View, cb: (error: Error, result?: any[]) => void): void { const materialized = this.searchMaterializedViews(view).sort((a, b) => { return (a.id < b.id) ? -1 : 1; }); const unique = [materialized[0]]; for (let i = 1; i < materialized.length; ++i) { if (materialized[i - 1].id !== materialized[i].id) { unique.push(materialized[i]); } } const query = this.buildQuery(view, unique); this.pool.connect((err, client, done) => { if (err) { cb (err); return; } client.query(query, [], (error, result) => { // call 'done()' to release client back to pool done(); cb(error, (result) ? result.rows : null); }); }); } public materializeView(view: View): boolean { return false; } private searchMaterializedViews(view: View): View[] { let r: View[] = []; if (view.materialized) { return [view]; } else { let children = view.childViews; for (let i = 0; i < children.length; ++i) { r = r.concat(this.searchMaterializedViews(children[i])); } } return r; } private buildQuery(target: View, views: View[]) { const metrics = target.metrics; const dimensions = target.dimensions; const clauses = target.clauses; let dimMap: {[key: string]: DimInfo} = {}; let metMap: {[key: string]: View[]} = {}; let nameMap: {[key: string]: View} = {}; for (let i = 0; i < views.length; ++i) { const mets = views[i].metrics; const dims = views[i].dimensions; for (let j = 0; j < mets.length; ++j) { if (!metMap[mets[j].name]) { metMap[mets[j].name] = [views[i]]; nameMap[mets[j].name] = views[i]; } else { metMap[mets[j].name].push(views[i]); } } for (let j = 0; j < dims.length; ++j) { if (!dimMap[dims[j].name]) { dimMap[dims[j].name] = { dim: dims[j], views: [views[i]] }; nameMap[dims[j].name] = views[i]; } else { dimMap[dims[j].name].views.push(views[i]); } } } // Projection const strMetrics = metrics.map((metric) => { const view = nameMap[metric.name]; let func = this.getAggregateFunction(metric.aggregation, view.origin); let quotedName = "\"" + metric.name + "\""; let extMetric = func + "(" + this.buildColumn(metric, view.id) + ")"; return extMetric + " AS " + quotedName; }); const parsedDimensions = dimensions.map((dimension) => { let dim = dimension; while (!nameMap[dim.name]) { dim = dim.parent; } const view = nameMap[dim.name]; const quotedName = "\"" + dimension.name + "\""; let extDimension = this.buildColumn(dim, view.id); let aux = dimension; while (aux.name !== dim.name) { extDimension = this.translateRelation(aux.relation, extDimension); aux = aux.parent; } return { aliased: extDimension + " AS " + quotedName, noalias: extDimension }; }); const strDimensions = parsedDimensions.map ((item) => item.aliased); const grouped = parsedDimensions.map((item) => item.noalias); const elements = strMetrics.concat(strDimensions); // Joins let conds: string[] = []; for (let i in dimMap) { let remainViews = dimMap[i].views.slice(); let dim = dimMap[i].dim; let leftSide = this.buildColumn(dim, remainViews.shift().id); if (remainViews.length > 0) { while (remainViews.length > 0) { const id = remainViews.shift().id; const rightSide = this.buildColumn(dim, id); conds.push(leftSide + " = " + rightSide); } } } // Selection let covered: Clause[] = []; for (let i = 0; i < views.length; ++i) { // Get the clauses that children already cover covered = covered.concat(views[i].clauses); } const toCover = clauses.filter((item) => !covered.some ((clause) => { return clause.id === item.id; })); toCover.forEach((item) => conds.push("(" + this.translateClause(item, nameMap) + ")")); // Assembly const projection = "SELECT " + elements.join(","); const source = " FROM " + views.map((view) => "view_" + view.id).join(","); const selection = (conds.length > 0) ? " WHERE " + conds.join(" AND ") : ""; let grouping = ""; if (grouped.length > 0) { grouping = " GROUP BY " + grouped.join(","); } return projection + source + selection + grouping + ";"; } private getAggregateFunction(aggrType: AggregationType, origin: boolean): string { switch (aggrType) { case AggregationType.SUM: return "SUM"; case AggregationType.AVG: return "AVG"; case AggregationType.COUNT: return (origin) ? "COUNT" : "SUM"; default: return ""; } } private translateRelation(relation: RelationType, arg: string): string { switch (relation) { case RelationType.DAY: return this.applyRelation("EXTRACT", ["DAY FROM "], [arg]); case RelationType.MONTH: return this.applyRelation("EXTRACT", ["MONTH FROM "], [arg]); case RelationType.YEAR: return this.applyRelation("EXTRACT", ["YEAR FROM "], [arg]); case RelationType.DAYOFWEEK: return this.applyRelation("EXTRACT", ["DOW FROM "], [arg]); default: return ""; } } private applyRelation(name: string, args: string[], values: string[]): string { /* This adapter uses the concept of functions in Postgres to implement BLENDB sub-dimention relations, this functions applys the transformation to build the call of a Postgres funtion. Note that this function can be native from postgres, like EXTRACT, or even implemented on the database. This function is short and only used in the translateRelation method however is a bit complex and is possible to be used several times, because of that is puted appart to make easyer update and avoid problems Example applyRelation ("EXTRACT", "["DAY FROM"]", ["view_0.date"]) output: EXTRACT(DAY FROM view_0.date) */ return name + "(" + args.map((item, idx) => item + values[idx]).join(",") + ")"; } private buildColumn (item: Metric|Dimension, id: string): string { const quotedName = "\"" + item.name + "\""; return "view_" + id + "." + quotedName; } private translateClause(clause: Clause, map: {[key: string]: View}): string { let r = clause.filters.map((item) => this.translateFilter(item, map)); return r.join(" OR "); } private translateFilter(filter: Filter, map: {[key: string]: View}): string { const viewId = map[filter.target.name].id; const leftSide = this.buildColumn(filter.target, viewId); const op = this.translateOperator(filter.operator); const dataType = this.translateDataType(filter.target.dataType); const quotedValue = "'" + filter.value + "'"; return leftSide + op + quotedValue + dataType; } private translateOperator(op: FilterOperator): string { switch (op) { case FilterOperator.EQUAL: return " = "; case FilterOperator.NOTEQUAL: return " != "; case FilterOperator.GREATER: return " > "; case FilterOperator.LOWER: return " < "; case FilterOperator.GREATEREQ: return " >= "; case FilterOperator.LOWEREQ: return " <= "; default: return ""; } } private translateDataType(dt: string ): string { switch (dt) { case "date": return "::DATE"; case "integer": return "::INTEGER"; case "boolean": return "::BOOLEAN"; default: return ""; } } }