Commit 1b7527f7 authored by Lucas Fernandes de Oliveira's avatar Lucas Fernandes de Oliveira
Browse files

Merge branch 'issue/11' into 'master'

Issue #11: Add engine support to sub-dimensions

See merge request !6
parents 28a9a8bb fe2dc93b
Pipeline #8907 passed with stage
in 30 seconds
......@@ -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
};
......@@ -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 : "";
}
}
......@@ -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);
});
});
......@@ -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,
......
......@@ -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());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment