From 1f5c6c5378421a93a719507680f48294d4cc63ad Mon Sep 17 00:00:00 2001
From: Lucas Fernandes de Oliveira <lfo14@inf.ufpr.br>
Date: Wed, 29 Mar 2017 11:02:42 -0300
Subject: [PATCH] Issue #9: Implement date parent relations in postgres adapter

Signed-off-by: Lucas Fernandes de Oliveira <lfo14@inf.ufpr.br>
---
 src/adapter/postgres.ts | 151 +++++++++++++++++++++++++++++-----------
 src/common/types.ts     |   5 +-
 src/core/dimension.ts   |   6 +-
 src/core/engine.spec.ts |  29 +++++---
 src/core/engine.ts      |  38 ++++++----
 src/core/view.ts        |  14 ++--
 6 files changed, 172 insertions(+), 71 deletions(-)

diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts
index b796d14..4c7e8b9 100644
--- a/src/adapter/postgres.ts
+++ b/src/adapter/postgres.ts
@@ -19,8 +19,8 @@
  */
 
 import { Adapter } from "../core/adapter";
-import { AggregationType } from "../common/types";
-import { View } from "../core/view";
+import { AggregationType, RelationType } from "../common/types";
+import { View, ChildView } from "../core/view";
 
 interface ParsedView {
     query: string;
@@ -30,44 +30,47 @@ interface ParsedView {
 export class PostgresAdapter extends Adapter{
     public getDataFromView(view: View): string {
         // buildQueryFromView does not put the final ;, it need to be put apart
-        return this.buildQueryFromView(view) + ";\n";
+        return this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n";
     }
 
     public materializeView(view: View): string {
         return null;
     }
 
-    private buildQueryFromView (view: View): string {
-        let sql = "(\n";
-        let metrics = view.metrics.map((metric: string) => {
-            let aggrType = view.getAggregationtype(metric);
-            let func = this.getAggregateFunction(aggrType);
-            let extMetric = func + "(" + metric + ")";
+    private buildQueryFromView (view: View, metrics: Metric[], dimensions: Dimession[]): 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);
+            let extMetric = func + "(" + metric.name + ")";
             return extMetric;
         });
 
         if (view.materialized) {
-            sql += "SELECT " + metrics.join(", ") + ", " + dimensions.join(", ") + "\n";
-            sql += "FROM " + "view_" + view.id + "\n";
-            sql += "GROUP BY " + view.dimensions.join(", ") + "\n";
+            let strDimensions = dimensions.map((dimmension) => dimension.name );
+            let sql = "(SELECT " + strMetrics.join(", ") + ", " + strDimensions.join(", ");
+            sql += " FROM " + "view_" + view.id;
+            sql += " GROUP BY " + strDimensions.join(", ");
             sql += ")\n";
             return sql;
         }
 
         else {
-            let children: ParsedView[] = view.childViews.map((item: View, idx: number) => {
+            let children: ParsedView[] = view.childViews.map((item: ChildView) => {
                 return {
-                    query: this.queryfyView(item, childAlias, degree + 1),
-                    view: item
+                    query: this.buildQueryFromView(item.view, item.metrics, item.dimensions),
+                    view: item.view
                 };
             });
 
             let covered = new Map();
-            view.dimensions.forEach((item: string) => covered.set(item, ""));
-            view.metrics.forEach((item: string) => covered.set(item, ""));
+            dimensions.forEach((item) => covered.set(item.name, ""));
+            metrics.forEach((item) => covered.set(item.name, ""));
 
             let projection = "SELECT ";
-            let viewsFrom = "FROM";
+            let viewsFrom = "FROM ";
             let selection = "WHERE ";
             let grouping = "GROUP BY ";
 
@@ -76,31 +79,64 @@ export class PostgresAdapter extends Adapter{
 
             children.forEach((child: ParsedView) => {
                 let selected = [];
-                child.view.dimensions.forEach((dimension: string) => {
-                    let first = covered.get(dimension);
-                    let extDimension = "view_" + child.view.id + "." + dimension;
-                    if (first === "") {
-                        covered.set(dimension, child.view.id);
-                        elements.push(extDimension);
-                        group.push(extDimension);
-                    }
+                let matchable = child.dimensions.map((item) => {
+                    return { match: item, sub: item, viewId: child.view.id };
+                });
 
-                    else {
-                        let extFirst = "view_" + first + "." + dimension;
-                        selected.push(extDimension + " = " + extFirst);
+                // Running over the dimensions that cover some part of the query
+                child.dimensions.forEach((dimension: Dimension) => {
+                    /*
+                        If a dimension has a parent, the parent must match with
+                        the sub dimension, so must be added in the matchable
+                        array, for the selection (WHERE statement).
+                    */
+                    let dim = dimension;
+                    while (dim.relation !== RelationType.NONE) {
+                        matchable.push({
+                            match: dim.name,
+                            sub: dimension,
+                            viewId: child.view.id
+                        });
+                        dim = dim.parent;
                     }
-                });
 
-                child.view.metrics.forEach((metric: string) => {
-                    let first = covered.get(metric);
-                    let aggrType = child.view.getAggregateFunction(metric);
-                    let func = this.geAggregateFunction(aggrType);
-                    let extMetric = func + "(view_" + child.view.id + "." + metric + ")";
-                    if (first === "") {
-                        covered.set(metric, child.view.id);
-                        elements.push(extMetric);
+                    dim = dimension;
+                    let extDimension = "view_" + child.view.id + ".\"" + dim.name + "\"";
+
+                    // If the view dimension does not match, search for parent
+                    while (!child.dimensions.some((item) => item.name === dim.name)) {
+                        dim = dim.parent;
+                        extDimension = this.translateRelation(dim.relation, extDimension);
                     }
+                    covered.set(dimension.name, extDimension);
+                    elements.push(extDimension + " AS " + dimension.name);
+                    group.push(dimension.name);
+                });
+                child.view.dimensions.forEach((dimension: Dimension) => {
+                    /*
+                        Make selection. Search for dimensions, that are in
+                        matchable array.
+                    */
+                    matchable.filter((item) => {
+                        return item.viewId !== child.view.id &&
+                                 item.match === dimension.name;
+                    })
+                    .forEach((item) => {
+                        // Expand the sub-dimension until match with a parent
+                        let dim = item.sub;
+                        let extDimension = "view_" + child.view.id + ".\"" + dimension.name + "\"";
+                        while (dim.name !== item.match) {
+                            dim = dim.parent;
+                            extDimension = this.translateRelation(dim, extDimension);
+                        }
+                        selected.push(extDimension + " = " + covered.get(item.sub.name));
+                    });
+                });
 
+                child.metrics.forEach((metric: Metric) => {
+                    let func = this.geAggregateFunction(metric.aggregation);
+                    let extMetric = func + "(view_" + child.view.id + "." + metric.name + ")";
+                    elements.push(extMetric);
                 });
 
                 viewsFrom += "\n" + child.query;
@@ -114,11 +150,8 @@ export class PostgresAdapter extends Adapter{
             selection += "\n";
             grouping += group.join(", ") + "\n";
 
-            sql += projection + viewsFrom + selection + grouping + ")";
-            return sql;
+            return "(" +  projection + viewsFrom + selection + grouping + ")";
         }
-
-        return sql;
     }
 
     private getAggregateFunction(aggrType: AggregationType): string {
@@ -134,4 +167,38 @@ export class PostgresAdapter extends Adapter{
         }
 
     }
+
+    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, args, values): 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(",") + ")";
+    }
 }
diff --git a/src/common/types.ts b/src/common/types.ts
index 6f07921..9757e7a 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -27,5 +27,8 @@ export enum AggregationType {
 
 export enum RelationType {
    NONE,
-   DAY
+   DAY,
+   MONTH,
+   YEAR,
+   DAYOFWEEK
 };
diff --git a/src/core/dimension.ts b/src/core/dimension.ts
index de6abcc..386d0b5 100644
--- a/src/core/dimension.ts
+++ b/src/core/dimension.ts
@@ -22,18 +22,18 @@ import { RelationType } from "../common/types";
 
 export interface DimensionOptions {
     name: string;
-    parent?: string;
+    parent?: Dimension;
     relation?: RelationType;
 }
 
 export class Dimension {
     public readonly name: string;
-    public readonly parent: string;
+    public readonly parent: Dimension;
     public readonly relation: RelationType;
 
     constructor(options: DimensionOptions) {
         this.name = options.name;
         this.relation = (options.relation) ? options.relation : RelationType.NONE;
-        this.parent = (options.parent) ? options.parent : "";
+        this.parent = (options.parent) ? options.parent : null;
     }
 }
diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts
index 5788e7c..8aaaddb 100644
--- a/src/core/engine.spec.ts
+++ b/src/core/engine.spec.ts
@@ -54,11 +54,11 @@ describe("engine class", () => {
     const dim10 = new Dimension({ name: "dim:10" });
     const dim11 = new Dimension({ name: "dim:11" });
 
-    const subdim1 = new Dimension({ name: "sub:1", parent: "dim:1", relation: RelationType.DAY });
-    const subdim2 = new Dimension({ name: "sub:2", parent: "dim:9", relation: RelationType.DAY });
-    const subdim3 = new Dimension({ name: "sub:3", parent: "sub:1", relation: RelationType.DAY });
-    const subdim4 = new Dimension({ name: "sub:4", parent: "sub:0", relation: RelationType.DAY });
-    const subdim5 = new Dimension({ name: "sub:5", parent: "dim:2", relation: RelationType.DAY });
+    const subdim1 = new Dimension({ name: "sub:1", parent: dim1, relation: RelationType.DAY });
+    const subdim2 = new Dimension({ name: "sub:2", parent: dim9, relation: RelationType.DAY });
+    const subdim3 = new Dimension({ name: "sub:3", parent: subdim1, relation: RelationType.DAY });
+    const subdim4 = new Dimension({ name: "sub:4", parent: null, relation: RelationType.DAY });
+    const subdim5 = new Dimension({ name: "sub:5", parent: dim2, relation: RelationType.DAY });
 
     engine.addMetric(met1);
     engine.addMetric(met2);
@@ -105,21 +105,32 @@ describe("engine class", () => {
         metrics: [met1, met2, met3, met4, met5],
         dimensions: [dim1, dim2],
         materialized: false,
-        childViews: [views[0], views[6]]
+        childViews: [
+            { view: views[0], metrics: [met1, met2, met3], dimensions: [dim1, dim2]},
+            { view: views[6], metrics: [met4], dimensions: []},
+            { view: views[1], metrics: [met5], dimensions: []}
+        ]
     }));
 
     views.push(new View({
         metrics: [met8, met9, met10],
         dimensions: [dim8, dim9, dim10],
         materialized: false,
-        childViews: [views[7], views[8], views[9]]
+        childViews: [
+            { view: views[7], metrics: [met8], dimensions: [dim8, dim9, dim10]},
+            { view: views[8], metrics: [met9], dimensions: []},
+            { view: views[9], metrics: [met10], dimensions: []}
+        ]
     }));
 
     views.push(new View({
         metrics: [met1],
         dimensions: [subdim1, subdim2],
         materialized: false,
-        childViews: [views[0], views[9]]
+        childViews: [
+            { view: views[0], metrics: [met1], dimensions: [dim1]},
+            { view: views[9], metrics: [], dimensions: [dim9]}
+        ]
     }));
 
     views.forEach((view) => engine.addView(view));
@@ -247,7 +258,7 @@ describe("engine class", () => {
         }
         catch (e){
             error = true;
-            expect(e.message).to.be.equal("The dimension named " + subdim4.parent + " was not found");
+            expect(e.message).to.be.equal("Engine sub-dimention " + subdim4.name + " with no parent");
 
         }
         expect(error);
diff --git a/src/core/engine.ts b/src/core/engine.ts
index 8fe8091..e60ef53 100644
--- a/src/core/engine.ts
+++ b/src/core/engine.ts
@@ -20,7 +20,7 @@
 
 import { Dimension } from "./dimension";
 import { Metric } from "./metric";
-import { View } from "./view";
+import { View, ChildView } from "./view";
 import { Query } from "../common/query";
 import { RelationType } from "../common/types";
 
@@ -77,12 +77,12 @@ export class Engine {
     private selectOptimalView (q: Query) {
         let metUncovered = q.metrics.map((met) => met);
         let dimUncovered = q.dimensions.map((dim) => dim);
-        let optimalViews: View[] = [];
+        let optimalViews: ChildView[] = [];
         let activeViews = this.getViews();
 
         // run this block until all metrics and dimmensions are covered
         while (metUncovered.length  > 0 || dimUncovered.length > 0) {
-            let bestView: View;
+            let bestView: ChildView;
             let coverLength = metUncovered.length + dimUncovered.length;
             let shortestDistance = coverLength + 1;
 
@@ -96,8 +96,11 @@ export class Engine {
                 let dimIntersection = dimUncovered.filter((dim: Dimension) => {
                     let r: boolean = view.dimensions.some((item) => item.name === dim.name);
                     while (!r && dim.relation !== RelationType.NONE) {
-                        r = view.dimensions.some((item) => item.name === dim.parent);
-                        dim = this.getDimensionByName(dim.parent);
+                        if (dim.parent === null) {
+                            throw new Error("Engine sub-dimention " + dim.name + " with no parent");
+                        }
+                        r = view.dimensions.some((item) => item.name === dim.parent.name);
+                        dim = dim.parent;
                     }
                     return r;
                 });
@@ -108,14 +111,22 @@ export class Engine {
                     let distance = coverLength - intersection;
 
                     if (distance < shortestDistance) {
-                        bestView = view;
+                        bestView = {
+                            view: view,
+                            metrics: metIntersection,
+                            dimensions: dimIntersection
+                        };
                         shortestDistance = distance;
                     }
 
                     else if (distance === shortestDistance &&
                             view.dimensions.length < bestView.dimensions.length) {
                         // priorizes views with less dimensions
-                        bestView = view;
+                        bestView = {
+                            view: view,
+                            metrics: metIntersection,
+                            dimensions: dimIntersection
+                        };
                     }
 
                     return true;
@@ -142,8 +153,11 @@ export class Engine {
             dimUncovered = dimUncovered.filter((dim: Dimension) => {
                 let r: boolean = bestView.dimensions.some((item) => item.name === dim.name);
                 while (!r && dim.relation !== RelationType.NONE) {
-                    r = bestView.dimensions.some((item) => item.name === dim.parent);
-                    dim = this.getDimensionByName(dim.parent);
+                    if (dim.parent === null) {
+                        throw new Error("Engine sub-dimention " + dim.name + " with no parent");
+                    }
+                    r = bestView.dimensions.some((item) => item.name === dim.parent.name);
+                    dim = dim.parent;
                 }
                 return !r;
             });
@@ -152,9 +166,9 @@ export class Engine {
         // If all the metrics and dimensions are the same and only exist one child view
         // return this single child view
         if (optimalViews.length === 1 &&
-            q.metrics.every((item) => optimalViews[0].metrics.indexOf(item) !== -1) &&
-            q.dimensions.every((item) => optimalViews[0].dimensions.indexOf(item) !== -1)) {
-            return optimalViews[0];
+            q.metrics.every((item) => optimalViews[0].view.metrics.indexOf(item) !== -1) &&
+            q.dimensions.every((item) => optimalViews[0].view.dimensions.indexOf(item) !== -1)) {
+            return optimalViews[0].view;
         }
         else {
             let options = {
diff --git a/src/core/view.ts b/src/core/view.ts
index 823fe24..66fd11d 100644
--- a/src/core/view.ts
+++ b/src/core/view.ts
@@ -22,11 +22,17 @@ import { Dimension } from "./dimension";
 import { Metric } from "./metric";
 import { Hash } from "../util/hash";
 
+export interface ChildView {
+    metrics: Metric[];
+    dimensions: Dimension[];
+    view: View;
+}
+
 export interface ViewOptions {
     metrics: Metric[];
     dimensions: Dimension[];
     materialized?: boolean;
-    childViews?: View[];
+    childViews?: ChildView[];
 }
 
 export class View {
@@ -34,7 +40,7 @@ export class View {
     public readonly metrics: Metric[];
     public readonly dimensions: Dimension[];
     public readonly materialized: boolean;
-    public childViews: View[];
+    public childViews: ChildView[];
 
     constructor (options: ViewOptions) {
         this.metrics = options.metrics.sort();
@@ -43,8 +49,8 @@ export class View {
         this.childViews = options.childViews || [];
 
         // calculate the id of the view based on it's metrics and dimensions
-        let metricsNames = this.metrics.map(metric => metric.name);
-        let dimensionsNames = this.dimensions.map(dimension => dimension.name);
+        let metricsNames = options.metrics.map(metric => metric.name);
+        let dimensionsNames = options.dimensions.map(dimension => dimension.name);
         this.id = Hash.sha1(metricsNames.concat(dimensionsNames).sort());
     }
 }
-- 
GitLab