Commit 0424536c authored by Lucas Fernandes de Oliveira's avatar Lucas Fernandes de Oliveira
Browse files

Issue #22: Change cover algorithm to use graph


Signed-off-by: Lucas Fernandes de Oliveira's avatarLucas Fernandes de Oliveira <lfo14@inf.ufpr.br>
parent 439c0e4c
Pipeline #10771 passed with stage
in 42 seconds
......@@ -140,7 +140,7 @@ describe("engine class", () => {
return optView.childViews[0].dimensions.some((item) => item.name === subdim[0].name);
});
expect(optimalView).satisfy((optView: View) => {
return optView.childViews[0].dimensions.some((item) => item.name === subdim[0].name);
return optView.childViews[0].dimensions.some((item) => item.name === subdim[1].name);
});
});
......@@ -177,7 +177,7 @@ describe("engine class", () => {
}
catch (e){
error = true;
expect(e.message).to.be.equal("Engine sub-dimention " + subdim[3].name + " with no parent");
expect(e.message).to.be.equal("Engine views cannot cover the query");
}
expect(error).to.be.true;
......
......@@ -20,29 +20,43 @@
import { Dimension } from "./dimension";
import { Metric } from "./metric";
import { View, ChildView } from "./view";
import { View } from "./view";
import { Query } from "../common/query";
import { RelationType } from "../common/types";
import { Graph } from "../util/graph";
export class Engine {
private views: View[] = [];
private metrics: Metric[] = [];
private dimensions: Dimension[] = [];
private graph: Graph;
constructor () { }
constructor () {
this.views = [];
this.metrics = [];
this.dimensions = [];
this.graph = new Graph();
}
public getViews() {
return this.views;
}
public addView(view: View) {
this.views.push(view);
return view;
if (this.graph.addView(view)) {
this.views.push(view);
return view;
}
return null;
}
public addMetric(metric: Metric) {
this.metrics.push(metric);
return metric;
if (this.graph.addMetric(metric)) {
this.metrics.push(metric);
return metric;
}
return null;
}
public getMetricByName(name: string) {
......@@ -56,8 +70,12 @@ export class Engine {
}
public addDimension(dimension: Dimension) {
this.dimensions.push(dimension);
return dimension;
if (this.graph.addDimension(dimension)) {
this.dimensions.push(dimension);
return dimension;
}
return null;
}
public getDimensionByName(name: string) {
......@@ -75,92 +93,9 @@ export class Engine {
}
private selectOptimalView (q: Query) {
let metUncovered = q.metrics.map((met) => met);
let dimUncovered = q.dimensions.map((dim) => dim);
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: ChildView;
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) => {
let metIntersection = metUncovered.filter((met: Metric) => {
return view.metrics.some((item) => item.name === met.name);
});
let dimIntersection = dimUncovered.filter((dim: Dimension) => {
let r: boolean = view.dimensions.some((item) => item.name === dim.name);
while (!r && dim.relation !== RelationType.NONE) {
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;
});
let intersection = metIntersection.length + dimIntersection.length;
if (intersection > 0) {
let distance = coverLength - intersection;
if (distance < shortestDistance) {
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: view,
metrics: metIntersection,
dimensions: dimIntersection
};
}
return true;
}
/*If the intersection is empty,
remove this element from future searches*/
return false;
});
if (shortestDistance === coverLength + 1) {
throw new Error("Engine views cannot cover the query");
}
optimalViews.push(bestView);
// remove metrics and dimensions corvered by the bestView from the
// objective (i.e. the object is already met for those metrics/dimensions)
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) {
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;
});
let optimalViews = this.graph.cover(q.metrics, q.dimensions);
if (optimalViews.length === 0) {
throw new Error ("Engine views cannot cover the query");
}
// If all the metrics and dimensions are the same and only exist one child view
......
/*
* Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blendb.
*
* blendb 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.
*
* blendb 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 blendb. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect } from "chai";
import { Metric } from "../core/metric";
import { Dimension } from "../core/dimension";
import { View } from "../core/view";
import { Graph } from "./graph";
import { AggregationType, RelationType } from "../common/types";
describe("graph class", () => {
it("should not create 2 vertices with the same dimension", () => {
let g = new Graph();
let dim = new Dimension({name: "dim:test", dataType: "string"});
expect(g.addDimension(dim)).to.be.true;
expect(g.addDimension(dim)).to.be.false;
});
it("should not create 2 vertices with same metric", () => {
let g = new Graph();
let met = new Metric({
name: "met:test",
aggregation: AggregationType.SUM,
dataType: "string"
});
expect(g.addMetric(met)).to.be.true;
expect(g.addMetric(met)).to.be.false;
});
it("should not create a vertex with a invalid dimension", () => {
let g = new Graph();
let dim: Dimension;
expect(g.addDimension(dim)).to.be.false;
});
it("should not create a vertex with a invalid metric", () => {
let g = new Graph();
let met: Metric;
expect(g.addMetric(met)).to.be.false;
});
it("should not create a vertex with a subdimension when parent is not a vertex", () => {
let g = new Graph();
let dim = new Dimension({name: "dim:test", dataType: "string"});
let subdim = new Dimension({
name: "dim:sub_test",
dataType: "string",
relation: RelationType.MONTH,
parent: dim
});
expect(g.addDimension(subdim)).to.be.false;
});
it("should add a set of views", () => {
let dims = [
new Dimension({name: "dim:0", dataType: "string"}),
new Dimension({name: "dim:1", dataType: "string"}),
new Dimension({name: "dim:2", dataType: "string"}),
new Dimension({name: "dim:3", dataType: "string"})
];
let g = new Graph();
for (let i = 0; i < dims.length; ++i) {
expect(g.addDimension(dims[i])).to.be.true;
}
let views = [
new View({
metrics: [],
dimensions: [dims[0], dims[1]],
materialized: true
}),
new View({
metrics: [],
dimensions: [dims[2], dims[3]],
materialized: true
}),
new View({
metrics: [],
dimensions: dims,
materialized: true
})
];
for (let i = 0; i < views.length; ++i) {
expect(g.addView(views[i])).to.be.true;
}
});
it("should not add a view twice", () => {
let dims = [
new Dimension({name: "dim:0", dataType: "string"}),
new Dimension({name: "dim:1", dataType: "string"}),
];
let g = new Graph();
for (let i = 0; i < dims.length; ++i) {
expect(g.addDimension(dims[i])).to.be.true;
}
let view = new View({
metrics: [],
dimensions: [dims[0], dims[1]],
materialized: true
});
expect(g.addView(view)).to.be.true;
expect(g.addView(view)).to.be.false;
});
it("should not add views when metrics and dimensions are not vertices", () => {
let dims = [
new Dimension({name: "dim:0", dataType: "string"}),
new Dimension({name: "dim:1", dataType: "string"}),
new Dimension({name: "dim:2", dataType: "string"})
];
let g = new Graph();
expect(g.addDimension(dims[0])).to.be.true;
let view = new View({
metrics: [],
dimensions: dims,
materialized: true
});
expect(g.addView(view)).to.be.false;
});
it("should not add invalid views", () => {
let g = new Graph();
expect(g.addView(null)).to.be.false;
});
it("should add only once a view with only one vertex", () => {
let dim = new Dimension({name: "dim:0", dataType: "string"});
let g = new Graph();
expect(g.addDimension(dim)).to.be.true;
let view = new View({
metrics: [],
dimensions: [dim],
materialized: true
});
expect(g.addView(view)).to.be.true;
expect(g.addView(view)).to.be.false;
});
it("should create a cover for a single vertex", () => {
let dim = new Dimension({name: "dim:0", dataType: "string"});
let g = new Graph();
expect(g.addDimension(dim)).to.be.true;
let view = new View({
metrics: [],
dimensions: [dim],
materialized: true
});
expect(g.addView(view)).to.be.true;
let children = g.cover([], [dim]);
expect(children).to.be.an("array");
expect(children).to.have.length(1);
expect(children[0].view.id).to.be.equal(view.id);
});
it("should create a cover for several vertices", () => {
let dims = [
new Dimension({name: "dim:0", dataType: "string"}),
new Dimension({name: "dim:1", dataType: "string"}),
new Dimension({name: "dim:2", dataType: "string"})
];
let mets = [
new Metric({name: "met:0", dataType: "integer", aggregation: AggregationType.SUM}),
new Metric({name: "met:1", dataType: "integer", aggregation: AggregationType.AVG}),
new Metric({name: "met:2", dataType: "integer", aggregation: AggregationType.AVG})
];
let g = new Graph();
for (let i = 0; i < 3; ++i) {
expect(g.addDimension(dims[i])).to.be.true;
expect(g.addMetric(mets[i])).to.be.true;
}
let views = [
new View({
metrics: [mets[0]],
dimensions: [dims[0]],
materialized: true
}),
new View({
metrics: [mets[1]],
dimensions: [dims[1]],
materialized: true
}),
new View({
metrics: [mets[2]],
dimensions: [dims[2]],
materialized: true
}),
new View({
metrics: [],
dimensions: dims,
materialized: true
}),
new View({
metrics: mets,
dimensions: dims,
materialized: true
}),
new View({
metrics: [mets[0], mets[1]],
dimensions: [dims[0], dims[1]],
materialized: true
}),
];
for (let i = 0; i < views.length; ++i) {
expect(g.addView(views[i])).to.be.true;
}
let children = g.cover([mets[0], mets[1]], [dims[0], dims[1]]);
expect(children).to.be.an("array");
expect(children).to.have.length(1);
expect(children[0].view.id).to.be.equal(views[views.length - 1].id);
});
it("should create a cover with sub dimensions", () => {
let dim = new Dimension({name: "dim:0", dataType: "date"});
let dims = [
dim,
new Dimension({
name: "subdim:0",
dataType: "string",
parent: dim,
relation: RelationType.MONTH
}),
new Dimension({
name: "subdim:1",
dataType: "string",
parent: dim,
relation: RelationType.DAY
}),
];
let g = new Graph();
for (let i = 0; i < 3; ++i) {
expect(g.addDimension(dims[i])).to.be.true;
}
let view = new View({
metrics: [],
dimensions: [dims[0]],
materialized: true
});
expect(g.addView(view)).to.be.true;
let children = g.cover([], [dims[1], dims[2]]);
expect(children).to.be.an("array");
expect(children).to.have.length(1);
expect(children[0].view.id).to.be.equal(view.id);
});
it("should return empty when try to cover a empty list", () => {
let dim = new Dimension({name: "dim:0", dataType: "date"});
let dims = [
dim,
new Dimension({
name: "subdim:0",
dataType: "string",
parent: dim,
relation: RelationType.MONTH
}),
new Dimension({
name: "subdim:1",
dataType: "string",
parent: dim,
relation: RelationType.DAY
}),
];
let g = new Graph();
for (let i = 0; i < 3; ++i) {
expect(g.addDimension(dims[i])).to.be.true;
}
let view = new View({
metrics: [],
dimensions: [dims[0]],
materialized: true
});
expect(g.addView(view)).to.be.true;
let children = g.cover([], []);
expect(children).to.be.an("array");
expect(children).to.be.empty;
});
});
/*
* Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blendb.
*
* blendb 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.
*
* blendb 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 blendb. If not, see <http://www.gnu.org/licenses/>.
*/
import { View, ChildView } from "../core/view";
import { Metric } from "../core/metric";
import { Dimension } from "../core/dimension";
enum State {
UNVISITED,
VISITED
}
/*
Class (interface) for a vertex in the graph
Created using a dimension or metric
Several of the attributes are used just for
search proposes like parent and state.
*/
interface Vertex {
id: string; // Name of vertex
neighbors: { [key: string]: Edge[] };
state: State; // During the search the vertex has a state
parent: Vertex; // During the search, what vertex "find" this
isSubDimension: boolean; // During insertion, was subdimension
dimensionParent: Vertex; // If is a subdimension, the vertex for that
dimension: Dimension; // If is a dimension, tha dimension that the vertex represents
metric: Metric; // If is a metric, the metric that the vertexrepresents
}
/*
Class (interface) dor a edge of the Graph
Normaly is possible to use a list of vertices
to represent the edges, however this edges have some
information different from the vertex, so a class
is needed.
Edges represent views or parent relationship with
dimensions and subdimensions.
*/
interface Edge {
isView: boolean; // True if edge represent view
view: View; // The view if isView is True
subDimension: Dimension; // The subDimension if isView is False
}
/*
Graph Class
Used to convert the Views Representation (Tables/Relations)
into a graph representation of the database schema.
Is not generic Graph, is not possible create vertex and
edges at will, only with metrics, dimensions and views.
Struct:
There is a vertex for each Metric and Dimension inserted
in the graph.
The graph is directed, but mostly vertices has edges in
both directions.
There is a edge between 2 vertices i and j in 3 cases:
I) if i and j are in the same view, there is a edge (i, j)
and (j, i)
II) if i is parent of the subdimention j , there is a
edge (i, j)
III) If a view has only one vertex, there is a loop (i, i)
So there is a edge if vertices are in the same view or
there is a parent relationship between vertices.
*/
export class Graph {
private vertices: Vertex[]; // vertices of the graph
private verticeMap: { [key: string]: Vertex };
// Map where key is the vertex id and the value is the vertex
public constructor () {