diff --git a/src/adapter/postgresAdapter.ts b/src/adapter/postgresAdapter.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e8cae4bafc475b1171e98b5caac23f6c76b178d --- /dev/null +++ b/src/adapter/postgresAdapter.ts @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + +import { View } from "../core/view"; +import { Adapter } from "../core/adapter"; +import { AggregationType } from "../common/aggregationType"; + +interface ParsedView { + query: string; + view: View; + alias: string; +}; + +export class PostGresAdapter extends Adapter{ + public getDataFromView(view: View): string { + return this.queryfyView(view, "v0"); + } + + public materializeView(view: View): string { + return null; + } + + private queryfyView (view: View, alias: string, degree: number = 0): string { + let ident = " ".repeat(degree + 1); + let lessIdent = " ".repeat(degree); + let sql: string = lessIdent + "(\n"; + let metrics: string[] = view.metrics.map((metric: string) => { + let func: string = this.metricFunction(view, metric); + let extMetric: string = func + "(" + metric + ")"; + return extMetric; + }); + let dimensions: string[] = view.dimensions.map((item: string) => { + return alias + "." + item; + }); + if (view.materialized) { + sql += ident + "SELECT " + metrics.join(", ") + ", " + dimensions.join(", ") + "\n"; + sql += ident + "FROM " + view.id + "\n"; + sql += ident + "GROUP BY " + dimensions.join(", ") + "\n"; + sql += lessIdent + ") AS " + alias + "\n"; + } + + else { + let children: ParsedView[] = view.childViews.map((item: View, idx: number) => { + let childAlias: string = "v" + idx; + return { + query: this.queryfyView(item, childAlias, degree + 1), + view: item, + alias: childAlias + }; + }); + + let covered: Map<string, string> = new Map(); + view.dimensions.forEach((item: string) => covered.set(item, "")); + view.metrics.forEach((item: string) => covered.set(item, "")); + + let projection: string = ident + "SELECT "; + let viewsFrom: string = ident + "FROM"; + let selection: string = ident + "WHERE "; + let grouping: string = ident + "GROUP BY "; + + let elements: string[] = []; + let group: string[] = []; + + children.forEach((child: ParsedView) => { + let selected: string[] = []; + child.view.dimensions.forEach((dimension: string) => { + let first: string = covered.get(dimension); + let extDimension = child.alias + "." + dimension; + if (first === "") { + covered.set(dimension, child.alias); + elements.push(extDimension); + group.push(extDimension); + } + + else { + let extFirst = first + "." + dimension; + selected.push(extDimension + " = " + extFirst); + } + }); + + child.view.metrics.forEach((metric: string) => { + let first: string = covered.get(metric); + let func: string = this.metricFunction(child.view, metric); + let extMetric: string = func + "(" + child.alias + "." + metric + ")"; + if (first === "") { + covered.set(metric, child.alias); + elements.push(extMetric); + } + + }); + + viewsFrom += "\n" + child.query; + + if (selected.length > 0) { + selection += selected.join(" AND "); + } + }); + + projection += elements.join(", ") + "\n"; + selection += "\n"; + grouping += group.join(", ") + "\n"; + alias += "\n"; + + sql += projection + viewsFrom + selection + grouping + lessIdent + ") AS " + alias; + } + + return sql; + } + + private metricFunction(view: View, metric: string): string{ + let map: Map<string, AggregationType> = view.aggregationMap; + let func: string = ""; + if (map.has(metric)) { + switch (map.get(metric)) { + case AggregationType.SUM: + func = "SUM"; + break; + case AggregationType.AVG: + func = "AVG"; + break; + case AggregationType.COUNT: + func = "COUNT"; + break; + default: + func = ""; + break; + } + } + + return func; + } +} diff --git a/src/common/aggregationType.ts b/src/common/aggregationType.ts new file mode 100644 index 0000000000000000000000000000000000000000..23b6f0b191393716f7f59efa43e036ae9b492a6c --- /dev/null +++ b/src/common/aggregationType.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + + export enum AggregationType { + SUM, + AVG, + COUNT + }; diff --git a/src/common/query.ts b/src/common/query.ts new file mode 100644 index 0000000000000000000000000000000000000000..70a46d5b749af87c11468de0bc53e47cea137b67 --- /dev/null +++ b/src/common/query.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + +export interface Query { + public metrics: string[]; + public dimensions: string[]; + +} diff --git a/src/core/adapter.ts b/src/core/adapter.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2981953b9f362da9e07cdab004454865facc304 --- /dev/null +++ b/src/core/adapter.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + +import { View } from "./view"; + +export abstract class Adapter { + public abstract getDataFromView(view: View): string; + public abstract materializeView(view: View): string; +} diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..94c6456d49dcd78de71b7ccb263f076bb3abcfdc --- /dev/null +++ b/src/core/engine.spec.ts @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015 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 { Engine } from "./engine"; +import { View } from "./view"; +import { ViewOptions } from "./view"; +import { Query } from "../common/query"; +import { AggregationType } from "../common/aggregationType"; + +describe("engine class", () => { + let query: Query = { + metrics: [], + dimensions: [] + }; + + let views: View[] = []; + + let viewBuilder: Query[] = [ + { metrics: ["met:1", "met:2", "met:3"], dimensions: ["dim:1", "dim:2"]}, + { metrics: ["met:1", "met:3", "met:5"], dimensions: ["dim:1", "dim:2"]}, + { metrics: ["met:3", "met:4", "met:7"], dimensions: ["dim:4", "dim:5"]}, + { metrics: ["met:6", "met:7"], dimensions: ["dim:3", "dim:4", "dim:5", "dim:6"]}, + { metrics: ["met:8", "met:2", "met:3"], dimensions: ["dim:1", "dim:2", "dim:7"]}, + { metrics: ["met:1", "met:2", "met:3"], dimensions: ["dim:1", "dim:2"]}, + { metrics: ["met:2", "met:4"], dimensions: ["dim:1", "dim:2"]}, + { metrics: ["met:8"], dimensions: ["dim:8", "dim:9", "dim:10"]}, + { metrics: ["met:9"], dimensions: ["dim:8", "dim:9", "dim:10"]}, + { metrics: ["met:10"], dimensions: ["dim:8", "dim:9", "dim:10"]} + ]; + + let iterable: [string, AggregationType][] = [ + ["met:1", AggregationType.SUM], + ["met:2", AggregationType.AVG], + ["met:3", AggregationType.AVG], + ["met:4", AggregationType.SUM], + ["met:5", AggregationType.SUM], + ["met:6", AggregationType.AVG], + ["met:7", AggregationType.COUNT], + ["met:8", AggregationType.COUNT], + ["met:9", AggregationType.SUM], + ["met:10", AggregationType.COUNT], + ]; + + let map: Map<string, AggregationType> = new Map(iterable); + + for (let i: number = 0; i < 10; ++ i) { + query.metrics.push("met:" + (i + 1)); + query.dimensions.push("dim:" + (i + 1)); + let options: ViewOptions = { + metrics: viewBuilder[i].metrics, + dimensions: viewBuilder[i].dimensions, + materialized: true, + aggregationMap: map, + childViews: [] + }; + views.push(new View (options)); + } + + views.push(new View({ + metrics: ["met:1", "met:2", "met:3", "met:4", "met:5"], + dimensions: ["dim:1", "dim:2"], + materialized: false, + aggregationMap: map, + childViews: [views[0], views[6]] + })); + + views.push(new View({ + metrics: ["met:8", "met:9", "met:10"], + dimensions: ["dim:8", "dim:9", "dim:10"], + materialized: false, + aggregationMap: map, + childViews: [views[7], views[8], views[9]] + })); + + it("should be create a fill that cover the query and has 4 children", () => { + let engine: Engine = new Engine (views); + 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 === 10); + expect(optimalView.dimensions.length === 10); + expect(optimalView.childViews.length === 4); + let metAux: number[] = optimalView.metrics.sort().map((item: string) => { + return Number(item.split(":")[1]); + }); + let dimAux: number[] = optimalView.dimensions.sort().map((item: string) => { + return Number(item.split(":")[1]); + }); + for (let i: number = 1; i <= 10; ++i) { + expect(dimAux[i] === i); + expect(metAux[i] === i); + } + }); + it("should throw an exception, query with non-existent metric", () => { + let engine: Engine = new Engine (views); + let error: boolean = false; + try { + engine.query({metrics: ["met:11"], dimensions: ["dim:1"]}); + } + catch (e){ + error = true; + expect(e.message).to.be.equal("Engine views cannot cover the query"); + + } + expect(error); + }); + + it("should throw an exception, query with non-existent dimension", () => { + let engine: Engine = new Engine (views); + let error: boolean = false; + try { + engine.query({metrics: ["met:1"], dimensions: ["dim:11"]}); + } + catch (e){ + error = true; + expect(e.message).to.be.equal("Engine views cannot cover the query"); + + } + expect(error); + }); +}); diff --git a/src/core/engine.ts b/src/core/engine.ts new file mode 100644 index 0000000000000000000000000000000000000000..55057a96165f158e9d7af24fb45791c1225285c0 --- /dev/null +++ b/src/core/engine.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + +import { Query } from "../common/query"; +import { View } from "./view"; +import { ViewOptions } from "./view"; +import { AggregationType } from "../common/aggregationType"; + +export class Engine { + private views: View[]; + + constructor (v: View[]) { + this.views = v; + } + + public query (q: Query) { return this.selectOptimalView(q); } + public insert (data: any) { /*to Implement*/ } + private selectOptimalView (q: Query) { + let objective: string[] = q.metrics.concat(q.dimensions); + let optimalViews: View[] = []; + let activeViews: View[] = this.views; + + while (objective.length > 0) { + let actual: View; + let actualDistance: number = objective.length + 1; + activeViews = activeViews.filter((view: View) => { + let intersection: string[] = []; + let cover: string[] = view.metrics.concat(view.dimensions); + intersection = cover.filter((item: string) => { + return objective.indexOf(item) !== -1; + }); + + if (intersection.length > 0) { + let distance: number = objective.length - intersection.length; + + if (distance < actualDistance) { + actual = view; + actualDistance = distance; + } + return true; + } + + return false; + }); + + if (actualDistance === objective.length + 1) { + throw new Error ("Engine views cannot cover the query"); + } + + optimalViews.push(actual); + objective = objective.filter((item: string) => { + let aux: string[] = actual.dimensions.concat(actual.metrics); + return aux.indexOf(item) === -1; + }); + } + + if (optimalViews.length === 1) { + return optimalViews.pop(); + } + + else { + let map: Map<string, AggregationType> = new Map(); + optimalViews.forEach((view: View) => { + view.aggregationMap.forEach((value: AggregationType, key: string) => { + map.set(key, value); + }); + }); + + let options: ViewOptions = { + metrics: q.metrics, + dimensions: q.dimensions, + materialized: false, + aggregationMap: map, + childViews: optimalViews + }; + + let view: View = new View(options); + this.views.push(view); + return view; + } + } +} diff --git a/src/core/view.ts b/src/core/view.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c0131a4ef1f9584aa6b2b83c140746287bb190f --- /dev/null +++ b/src/core/view.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blend. + * + * blend 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. + * + * blend 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 blend. If not, see <http://www.gnu.org/licenses/>. + */ + +import { Hash } from "../util/hash"; +import { AggregationType } from "../common/aggregationType"; + +export interface ViewOptions { + metrics: string[]; + dimensions: string[]; + materialized: boolean; + aggregationMap: Map <string, AggregationType>; + childViews: View[]; +} + +export class View { + public readonly id: string; + public readonly metrics: string[]; + public readonly dimensions: string[]; + public readonly materialized: boolean; + public readonly aggregationMap: Map<string, AggregationType>; + + public childViews: View[]; + + constructor (options: ViewOptions) { + this.metrics = options.metrics; + this.dimensions = options.dimensions; + this.materialized = options.materialized; + this.childViews = options.childViews; + this.aggregationMap = options.aggregationMap; + this.id = "view_" + Hash.sha1(options.metrics.sort(), options.dimensions.sort()); + } + +}