diff --git a/src/common/types.ts b/src/common/types.ts index 8197a08687863b45b8a99a465d367f351d8cfc0a..6f07921e49bc11ca56dad2cb8d95daf25338688d 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -18,9 +18,14 @@ * along with blend. If not, see <http://www.gnu.org/licenses/>. */ - export enum AggregationType { - SUM, - AVG, - COUNT, - NONE - }; +export enum AggregationType { + NONE, + SUM, + AVG, + COUNT +}; + +export enum RelationType { + NONE, + DAY +}; diff --git a/src/core/dimension.ts b/src/core/dimension.ts index f0b27898fb363ef38594430e61afe40f52dd182f..de6abccebeba0f0cef4c01208c6b2bf63555059e 100644 --- a/src/core/dimension.ts +++ b/src/core/dimension.ts @@ -18,14 +18,22 @@ * along with blend. If not, see <http://www.gnu.org/licenses/>. */ +import { RelationType } from "../common/types"; + export interface DimensionOptions { name: string; + parent?: string; + relation?: RelationType; } export class Dimension { public readonly name: string; + public readonly parent: string; + 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 : ""; } } diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index 2b9d24b6a0891c1705b34c2a77590d5d284b3f19..5788e7c9dd95b63abfcf4c4bf93dcc807d2fad2a 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -25,6 +25,7 @@ import { Metric } from "./metric"; import { Dimension } from "./dimension"; import { View } from "./view"; import { AggregationType } from "../common/types"; +import { RelationType } from "../common/types"; describe("engine class", () => { const engine = new Engine(); @@ -53,6 +54,12 @@ 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 }); + engine.addMetric(met1); engine.addMetric(met2); engine.addMetric(met3); @@ -75,6 +82,12 @@ describe("engine class", () => { engine.addDimension(dim9); engine.addDimension(dim10); + engine.addDimension(subdim1); + engine.addDimension(subdim2); + engine.addDimension(subdim3); + engine.addDimension(subdim4); + engine.addDimension(subdim5); + let views: View[] = [ new View({ metrics: [met1, met2, met3], dimensions: [dim1, dim2]}), new View({ metrics: [met1, met3, met5], dimensions: [dim1, dim2]}), @@ -102,6 +115,13 @@ describe("engine class", () => { childViews: [views[7], views[8], views[9]] })); + views.push(new View({ + metrics: [met1], + dimensions: [subdim1, subdim2], + materialized: false, + childViews: [views[0], views[9]] + })); + views.forEach((view) => engine.addView(view)); it("should be create a fill that cover the query and has 4 children", () => { @@ -156,4 +176,80 @@ describe("engine class", () => { } expect(error); }); + + it("should be create a fill that cover the query, that match perfectly with a existent view", () => { + let query = { + metrics : [met1, met2, met3] + , dimensions : [dim1, dim2] + }; + let optimalView = engine.query(query); + expect(optimalView).to.be.an("object"); + expect(optimalView).to.have.property("metrics"); + expect(optimalView).to.have.property("dimensions"); + expect(optimalView).to.have.property("childViews"); + expect(optimalView.metrics).to.be.an("array"); + expect(optimalView.dimensions).to.be.an("array"); + expect(optimalView.childViews).to.be.an("array"); + expect(optimalView.metrics.length === 3); + expect(optimalView.dimensions.length === 2); + expect(optimalView.childViews.length === 0); + + expect(optimalView.id === views[0].id); + }); + + it("should be create a fill that cover the query, using sub dimensions", () => { + let emptyMetrics: Metric[] = []; + let query = { + metrics : emptyMetrics + , dimensions : [subdim1, subdim2] + }; + let optimalView = engine.query(query); + expect(optimalView).to.be.an("object"); + expect(optimalView).to.have.property("metrics"); + expect(optimalView).to.have.property("dimensions"); + expect(optimalView).to.have.property("childViews"); + expect(optimalView.metrics).to.be.an("array"); + expect(optimalView.dimensions).to.be.an("array"); + expect(optimalView.childViews).to.be.an("array"); + expect(optimalView.metrics.length === 0); + expect(optimalView.dimensions.length === 2); + expect(optimalView.childViews.length === 1); + + expect(optimalView.childViews[0].dimensions.some((item) => item.name === subdim1.name)); + expect(optimalView.childViews[0].dimensions.some((item) => item.name === subdim2.name)); + }); + + it("should be create a fill that cover the query, using the parents of sub dimensions", () => { + let emptyMetrics: Metric[] = []; + let query = { + metrics : emptyMetrics + , dimensions : [subdim3, subdim5] + }; + let optimalView = engine.query(query); + expect(optimalView).to.be.an("object"); + expect(optimalView).to.have.property("metrics"); + expect(optimalView).to.have.property("dimensions"); + expect(optimalView).to.have.property("childViews"); + expect(optimalView.metrics).to.be.an("array"); + expect(optimalView.dimensions).to.be.an("array"); + expect(optimalView.childViews).to.be.an("array"); + expect(optimalView.metrics.length === 0); + expect(optimalView.dimensions.length === 2); + expect(optimalView.childViews.length === 1); + + expect(optimalView.id === views[0].id); + }); + + it("should throw an exception, sub-dimension with non-existent parent", () => { + let error: boolean = false; + try { + engine.query({metrics: [met11], dimensions: [subdim4]}); + } + catch (e){ + error = true; + expect(e.message).to.be.equal("The dimension named " + subdim4.parent + " was not found"); + + } + expect(error); + }); }); diff --git a/src/core/engine.ts b/src/core/engine.ts index e810f570b64e1cf022961217d766c6d460dbad1e..8fe8091b68b0d88b8bac32617eb8cb02a64249d8 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -22,6 +22,7 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; import { View } from "./view"; import { Query } from "../common/query"; +import { RelationType } from "../common/types"; export class Engine { private views: View[] = []; @@ -74,34 +75,49 @@ export class Engine { } private selectOptimalView (q: Query) { - let metricsName = q.metrics.map((met) => met.name); - let dimensionsName = q.dimensions.map((dim) => dim.name); - let objective = metricsName.concat(dimensionsName); + let metUncovered = q.metrics.map((met) => met); + let dimUncovered = q.dimensions.map((dim) => dim); let optimalViews: View[] = []; let activeViews = this.getViews(); // run this block until all metrics and dimmensions are covered - while (objective.length > 0) { + while (metUncovered.length > 0 || dimUncovered.length > 0) { let bestView: View; - let shortestDistance = objective.length + 1; + let coverLength = metUncovered.length + dimUncovered.length; + let shortestDistance = coverLength + 1; // remove views from the activeViews if they don't intersect // with the objective activeViews = activeViews.filter((view: View) => { - metricsName = view.metrics.map((met) => met.name); - dimensionsName = view.dimensions.map((dim) => dim.name); - let cover = metricsName.concat(dimensionsName); - let intersection = cover.filter((item: string) => { - return objective.indexOf(item) !== -1; + let metIntersection = metUncovered.filter((met: Metric) => { + return view.metrics.some((item) => item.name === met.name); }); - if (intersection.length > 0) { - let distance = objective.length - intersection.length; + 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); + } + return r; + }); + + let intersection = metIntersection.length + dimIntersection.length; + + if (intersection > 0) { + let distance = coverLength - intersection; if (distance < shortestDistance) { bestView = view; shortestDistance = distance; } + + else if (distance === shortestDistance && + view.dimensions.length < bestView.dimensions.length) { + // priorizes views with less dimensions + bestView = view; + } + return true; } @@ -110,7 +126,7 @@ export class Engine { return false; }); - if (shortestDistance === objective.length + 1) { + if (shortestDistance === coverLength + 1) { throw new Error("Engine views cannot cover the query"); } @@ -118,22 +134,29 @@ export class Engine { // remove metrics and dimensions corvered by the bestView from the // objective (i.e. the object is already met for those metrics/dimensions) - objective = objective.filter((item: string) => { - metricsName = bestView.metrics.map((met) => met.name); - dimensionsName = bestView.dimensions.map((dim) => dim.name); - let cover = dimensionsName.concat(metricsName); - return cover.indexOf(item) === -1; + + metUncovered = metUncovered.filter((met: Metric) => { + return !bestView.metrics.some((item) => item.name === met.name); + }); + + 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); + } + return !r; }); } - if (optimalViews.length === 1) { - // if there is a single view that covers the query, we just return it - return optimalViews.pop(); + // 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]; } else { - // if more than one view is necessary to cover the query, - // we need to compose them into a new singular virtual view - let options = { metrics: q.metrics, dimensions: q.dimensions, diff --git a/src/core/view.ts b/src/core/view.ts index b1f5fb60e511ac548cd3ec852bad6b76e2f0b3cc..823fe240f3530c42e204d5eeceee600ced9665a0 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -37,14 +37,14 @@ export class View { public childViews: View[]; constructor (options: ViewOptions) { - this.metrics = options.metrics; - this.dimensions = options.dimensions; + this.metrics = options.metrics.sort(); + this.dimensions = options.dimensions.sort(); this.materialized = options.materialized || true; 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); - this.id = Hash.sha1(metricsNames.concat(dimensionsNames)); + this.id = Hash.sha1(metricsNames.concat(dimensionsNames).sort()); } }