diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts index 685963c48d1c85d59647f35c9289ea065584446a..fc5af7bbd14e467357ac171fcb0147c73dc3ff36 100644 --- a/src/adapter/postgres.ts +++ b/src/adapter/postgres.ts @@ -22,16 +22,13 @@ import { Adapter } from "../core/adapter"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; import { AggregationType, RelationType } from "../common/types"; -import { View, ChildView } from "../core/view"; +import { View } from "../core/view"; import { Pool, PoolConfig } from "pg"; -interface ParsedChild { - query: string; - view: View; - dimensions: Dimension[]; - metrics: Metric[]; - alias: string; -}; +interface DimInfo { + dim: Dimension; + views: View[]; +} export class PostgresAdapter extends Adapter { private pool: Pool; @@ -42,7 +39,20 @@ export class PostgresAdapter extends Adapter { } public getDataFromView(view: View, cb: (error: Error, result?: any[]) => void): void { // buildQueryFromView does not put the final ;, it need to be put apart - let query = this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n"; + // let query = this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n"; + + 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); @@ -60,136 +70,6 @@ export class PostgresAdapter extends Adapter { return false; } - private buildQueryFromView (view: View, metrics: Metric[], dimensions: Dimension[]): string { - /* - Reduce metrics and dimensions array to the intersection with the - view. So is possible only get useful data in the sub-querys. - */ - let strMetrics = metrics.map((metric) => { - let func = this.getAggregateFunction(metric.aggregation, view.origin); - let quotedName = "\"" + metric.name + "\""; - let extMetric = func + "(" + quotedName + ") AS " + quotedName; - return extMetric; - }); - - if (view.materialized) { - let strDimensions = dimensions.map((dimension) => "\"" + dimension.name + "\""); - let sql = "(SELECT " + strMetrics.concat(strDimensions).join(", "); - sql += " FROM " + "view_" + view.id; - if (strDimensions.length > 0 && strMetrics.length > 0) { - sql += " GROUP BY " + strDimensions.join(", "); - } - sql += ")"; - return sql; - } - - else { - - let covered = new Map(); - let matchable: any[] = [] ; - dimensions.forEach((item) => { - covered.set(item.name, ""); - /* - For each dimension that must be covered - create a match in the array. - If a more than onde view match to the same - dimension, cretes a WHERE clause - */ - matchable.push({match: item.name, sub: item}); - let dim = item; - /* - Sub dimensions also have parents that can match - with then too. - */ - while (dim.relation !== RelationType.NONE) { - dim = dim.parent; - matchable.push({ - match: dim.name, - sub: item, - }); - } - }); - metrics.forEach((item) => covered.set(item.name, "")); - - let elements: string[] = []; - let group: string[] = []; - let viewsQuery: string[] = []; - let selected: string[] = []; - - let children: ParsedChild[] = view.childViews.map((item: ChildView) => { - let dims = item.view.dimensions.filter((dim) => { - return matchable.some((match) => match.match === dim.name); - }); - - let mets = item.metrics.filter((met) => { - return metrics.some((elem) => elem.name === met.name); - }); - let query = ""; - if (dims.length !== 0 || mets.length !== 0) { - query = this.buildQueryFromView(item.view, mets, dims); - } - return { - query: query, - view: item.view, - dimensions : dims, - metrics: mets, - alias: "alias_" + item.view.id - }; - }).filter ((item) => item.query !== ""); - - children.forEach((child: ParsedChild) => { - child.view.dimensions.forEach((dimension: Dimension) => { - /* - Make selection. Search for dimensions, that are in - matchable array. - */ - matchable.filter((item) => { - return item.match === dimension.name; - }) - .forEach((item) => { - // Expand the sub-dimension until match with a parent - let dim = item.sub; - let extDimension = child.alias + ".\"" + dimension.name + "\""; - while (dim.name !== item.match) { - extDimension = this.translateRelation(dim.relation, extDimension); - dim = dim.parent; - } - - if (covered.get(item.sub.name) === "") { - elements.push(extDimension + " AS \"" + item.sub.name + "\""); - covered.set(item.sub.name, extDimension); - group.push(extDimension); - } - else { - selected.push(extDimension + " = " + covered.get(item.sub.name)); - } - }); - }); - - child.metrics.forEach((metric: Metric) => { - // Only materialized views can have origin as true - let func = this.getAggregateFunction(metric.aggregation, false); - let quotedName = "\"" + metric.name + "\""; - let extMetric = func + "(" + child.alias + "." + quotedName + ") AS " + quotedName; - elements.push(extMetric); - }); - - viewsQuery.push(child.query + " AS " + child.alias); - - }); - - let projection = "SELECT " + elements.join(", ") + "\n"; - let viewsFrom = "FROM " + viewsQuery.join(", ") + "\n"; - let selection = (selected.length > 0) ? "WHERE " + selected.join(" AND ") + "\n" : ""; - let grouping = ""; - if (group.length > 0 && metrics.length > 0) { - grouping = "GROUP BY " + group.join(", ") + "\n"; - } - - return "(" + projection + viewsFrom + selection + grouping + ")"; - } - } - private getAggregateFunction(aggrType: AggregationType, origin: boolean): string { switch (aggrType) { case AggregationType.SUM: @@ -237,4 +117,116 @@ export class PostgresAdapter extends Adapter { */ return name + "(" + args.map((item, idx) => item + values[idx]).join(",") + ")"; } + + private searchMaterializedViews(view: View): View[] { + let r: View[] = []; + if (view.materialized) { + return [view]; + } + + else { + let children = view.childViews.map((item) => item.view); + 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; + + let dimMap: {[key: string]: DimInfo} = {}; + let metMap: {[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]]; + } + + 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]] + }; + } + + else { + dimMap[dims[j].name].views.push(views[i]); + } + } + } + + const strMetrics = metrics.map((metric) => { + const view = metMap[metric.name][0]; + let func = this.getAggregateFunction(metric.aggregation, view.origin); + let quotedName = "\"" + metric.name + "\""; + let extMetric = func + "(view_" + view.id + "." + quotedName + ")"; + return extMetric + " AS " + quotedName; + }); + + const parsedDimensions = dimensions.map((dimension) => { + let dim = dimension; + while (!dimMap[dim.name]) { + // Checar exeção + dim = dim.parent; + } + const view = dimMap[dim.name].views[0]; + const quotedDim = "\"" + dim.name + "\""; + const quotedName = "\"" + dimension.name + "\""; + let extDimension = "view_" + view.id + "." + quotedDim; + 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); + + let joins = []; + 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); + joins.push(leftSide + " = " + rightSide); + } + } + + } + + const projection = "SELECT " + elements.join(","); + const source = " FROM " + views.map((view) => "view_" + view.id).join(","); + const selection = (joins.length > 0) ? " WHERE " + joins.join(" AND ") : ""; + let grouping = ""; + if (grouped.length > 0) { + grouping = " GROUP BY " + grouped.join(","); + } + + return projection + source + selection + grouping + ";"; + + } + + private buildColumn (item: Metric|Dimension, id: string): string { + const quotedName = "\"" + item.name + "\""; + return "view_" + id + "." + quotedName; + } } diff --git a/test/scenario.ts b/test/scenario.ts index 45a428c19f13a2e1a8bccf195897380281f846d6..1e5e33e0ab67d96384ab6b2469f7e4bc9cd8f703 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -152,6 +152,11 @@ const aggrView = new View({ view: views[2], metrics: [mets[6]], dimensions: [] + }, + { + view: views[4], + metrics: [], + dimensions: [] } ] });