diff --git a/src/adapter/postgres.spec.ts b/src/adapter/postgres.spec.ts index cae97a84b9676aef05865d63c8a43ab783a279cf..18583960504ee4918d1b68060a6b78c77c82a870 100644 --- a/src/adapter/postgres.spec.ts +++ b/src/adapter/postgres.spec.ts @@ -169,4 +169,76 @@ describe("postgres adapter", () => { done(); }); }); + + it("should get data from view when a single clause exists", (done) => { + let view = adapterScenario.clauseView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(1); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + expect(parseInt(result[0]["met:0"], 10)).to.be.equal(1); + expect(parseInt(result[0]["met:1"], 10)).to.be.equal(1); + expect(parseInt(result[0]["met:2"], 10)).to.be.equal(1); + expect(result[0]["dim:0"].getDate()).to.be.equal(1); + done(); + }); + }); + + it("should get data from view with single clause, with more than on filter", (done) => { + let view = adapterScenario.multiFilterView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(2); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + done(); + }); + }); + + it("should get data from view with multiple clauses", (done) => { + let view = adapterScenario.multiClauseView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(0); + + done(); + }); + }); + + it("should get data from view with a clause with not equal operator", (done) => { + let view = adapterScenario.notEqualView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(4); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + done(); + }); + }); }); diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts index d12c6894a930d0408e98892d09c93a43ce7080fd..df8623e22312a8740a489fb76e6158ace1c86792 100644 --- a/src/adapter/postgres.ts +++ b/src/adapter/postgres.ts @@ -21,6 +21,8 @@ import { Adapter } from "../core/adapter"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; +import { Clause } from "../core/clause"; +import { Filter, FilterOperator } from "../core/filter"; import { AggregationType, RelationType } from "../common/types"; import { View } from "../core/view"; import { Pool, PoolConfig } from "pg"; @@ -38,9 +40,6 @@ export class PostgresAdapter extends Adapter { this.pool = new Pool(config); } public getDataFromView(view: View, cb: (error: Error, result?: any[]) => void): void { - // buildQueryFromView does not put the final ;, it need to be put apart - // let query = this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n"; - const materialized = this.searchMaterializedViews(view).sort((a, b) => { return (a.id < b.id) ? -1 : 1; }); @@ -70,54 +69,6 @@ export class PostgresAdapter extends Adapter { return false; } - private getAggregateFunction(aggrType: AggregationType, origin: boolean): string { - switch (aggrType) { - case AggregationType.SUM: - return "SUM"; - case AggregationType.AVG: - return "AVG"; - case AggregationType.COUNT: - return (origin) ? "COUNT" : "SUM"; - default: - return ""; - } - - } - - 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: string, args: string[], values: string[]): 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(",") + ")"; - } - private searchMaterializedViews(view: View): View[] { let r: View[] = []; if (view.materialized) { @@ -137,9 +88,11 @@ export class PostgresAdapter extends Adapter { private buildQuery(target: View, views: View[]) { const metrics = target.metrics; const dimensions = target.dimensions; + const clauses = target.clauses; let dimMap: {[key: string]: DimInfo} = {}; let metMap: {[key: string]: View[]} = {}; + let nameMap: {[key: string]: View} = {}; for (let i = 0; i < views.length; ++i) { const mets = views[i].metrics; @@ -147,6 +100,7 @@ export class PostgresAdapter extends Adapter { for (let j = 0; j < mets.length; ++j) { if (!metMap[mets[j].name]) { metMap[mets[j].name] = [views[i]]; + nameMap[mets[j].name] = views[i]; } else { @@ -160,6 +114,7 @@ export class PostgresAdapter extends Adapter { dim: dims[j], views: [views[i]] }; + nameMap[dims[j].name] = views[i]; } else { @@ -168,24 +123,23 @@ export class PostgresAdapter extends Adapter { } } + // Projection const strMetrics = metrics.map((metric) => { - const view = metMap[metric.name][0]; + const view = nameMap[metric.name]; let func = this.getAggregateFunction(metric.aggregation, view.origin); let quotedName = "\"" + metric.name + "\""; - let extMetric = func + "(view_" + view.id + "." + quotedName + ")"; + let extMetric = func + "(" + this.buildColumn(metric, view.id) + ")"; return extMetric + " AS " + quotedName; }); const parsedDimensions = dimensions.map((dimension) => { let dim = dimension; - while (!dimMap[dim.name]) { - // Checar exeção + while (!nameMap[dim.name]) { dim = dim.parent; } - const view = dimMap[dim.name].views[0]; - const quotedDim = "\"" + dim.name + "\""; + const view = nameMap[dim.name]; const quotedName = "\"" + dimension.name + "\""; - let extDimension = "view_" + view.id + "." + quotedDim; + let extDimension = this.buildColumn(dim, view.id); let aux = dimension; while (aux.name !== dim.name) { extDimension = this.translateRelation(aux.relation, extDimension); @@ -198,7 +152,8 @@ export class PostgresAdapter extends Adapter { const grouped = parsedDimensions.map((item) => item.noalias); const elements = strMetrics.concat(strDimensions); - let joins = []; + // Joins + let conds: string[] = []; for (let i in dimMap) { let remainViews = dimMap[i].views.slice(); let dim = dimMap[i].dim; @@ -207,15 +162,30 @@ export class PostgresAdapter extends Adapter { while (remainViews.length > 0) { const id = remainViews.shift().id; const rightSide = this.buildColumn(dim, id); - joins.push(leftSide + " = " + rightSide); + conds.push(leftSide + " = " + rightSide); } } } + // Selection + let covered: Clause[] = []; + for (let i = 0; i < views.length; ++i) { + // Get the clauses that children already cover + covered = covered.concat(views[i].clauses); + } + + const toCover = clauses.filter((item) => !covered.some ((clause) => { + return clause.id === item.id; + })); + + toCover.forEach((item) => conds.push("(" + this.translateClause(item, nameMap) + ")")); + + // Assembly + const projection = "SELECT " + elements.join(","); const source = " FROM " + views.map((view) => "view_" + view.id).join(","); - const selection = (joins.length > 0) ? " WHERE " + joins.join(" AND ") : ""; + const selection = (conds.length > 0) ? " WHERE " + conds.join(" AND ") : ""; let grouping = ""; if (grouped.length > 0) { grouping = " GROUP BY " + grouped.join(","); @@ -225,8 +195,93 @@ export class PostgresAdapter extends Adapter { } + private getAggregateFunction(aggrType: AggregationType, origin: boolean): string { + switch (aggrType) { + case AggregationType.SUM: + return "SUM"; + case AggregationType.AVG: + return "AVG"; + case AggregationType.COUNT: + return (origin) ? "COUNT" : "SUM"; + default: + return ""; + } + + } + + 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: string, args: string[], values: string[]): 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(",") + ")"; + } + private buildColumn (item: Metric|Dimension, id: string): string { const quotedName = "\"" + item.name + "\""; return "view_" + id + "." + quotedName; } + + private translateClause(clause: Clause, map: {[key: string]: View}): string { + let r = clause.filters.map((item) => this.translateFilter(item, map)); + return r.join(" OR "); + } + + private translateFilter(filter: Filter, map: {[key: string]: View}): string { + const viewId = map[filter.target.name].id; + const leftSide = this.buildColumn(filter.target, viewId); + const op = this.translateOperator(filter.operator); + const dataType = this.translateDataType(filter.target.dataType); + const quotedValue = "'" + filter.value + "'"; + return leftSide + op + quotedValue + dataType; + } + + private translateOperator(op: FilterOperator): string { + switch (op) { + case FilterOperator.EQUAL: + return " = "; + case FilterOperator.NOTEQUAL: + return " != "; + default: + return ""; + } + + } + + private translateDataType(dt: string ): string { + switch (dt) { + case "date": + return "::DATE"; + case "integer": + return "::INTEGER"; + default: + return ""; + } + } } diff --git a/src/api/controllers/data.spec.ts b/src/api/controllers/data.spec.ts index c9aec893fdf9b42957fc446c023aad191449a062..4e1ca99c6b2403f422812508dfaa3e22d695f70b 100644 --- a/src/api/controllers/data.spec.ts +++ b/src/api/controllers/data.spec.ts @@ -28,6 +28,7 @@ import { Query } from "../../common/query"; interface StrQuery { metrics: string; dimensions: string; + filters?: string; } function parseQuery(obj: Query): StrQuery { @@ -94,4 +95,79 @@ describe("API data controller", () => { .end(done); }); + it("should respond 200 and get some data, using a single filter", (done) => { + // Clause does not come to scenario besause is a lot of work for + // only a single test + let query = parseQuery(tests.clausal); + query.filters = "dim:7==1"; + request(server) + .get("/v1/data") + .query(query) + .expect(200) + .expect((res: any) => { + let result = res.body; + expect(result).to.be.an("array"); + expect(result).to.have.length(1); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(tests.clausal.metrics.map((item) => item.name)); + keys = keys.concat(tests.clausal.dimensions.map((item) => item.name)); + result.forEach((row: any) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + }) + .end(done); + }); + + it("should respond 200 and get some data, using filters with OR", (done) => { + // Clause does not come to scenario besause is a lot of work for + // only a single test + let query = parseQuery(tests.clausal); + query.filters = "dim:7==1,dim:7==2"; + request(server) + .get("/v1/data") + .query(query) + .expect(200) + .expect((res: any) => { + let result = res.body; + expect(result).to.be.an("array"); + expect(result).to.have.length(2); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(tests.clausal.metrics.map((item) => item.name)); + keys = keys.concat(tests.clausal.dimensions.map((item) => item.name)); + result.forEach((row: any) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + }) + .end(done); + }); + + it("should respond 200 and get some data, using filters with AND", (done) => { + // Clause does not come to scenario besause is a lot of work for + // only a single test + let query = parseQuery(tests.clausal); + query.filters = "dim:7!=1;dim:0!=2017-01-01"; + request(server) + .get("/v1/data") + .query(query) + .expect(200) + .expect((res: any) => { + let result = res.body; + expect(result).to.be.an("array"); + expect(result).to.have.length(4); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(tests.clausal.metrics.map((item) => item.name)); + keys = keys.concat(tests.clausal.dimensions.map((item) => item.name)); + result.forEach((row: any) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + }) + .end(done); + }); + }); diff --git a/src/api/controllers/data.ts b/src/api/controllers/data.ts index 285fc91e28e17cf6e50648c27a85bceccdef0f8a..97b55a49cfb90964454cb13076b2368216574db9 100644 --- a/src/api/controllers/data.ts +++ b/src/api/controllers/data.ts @@ -26,10 +26,14 @@ export class DataCtrl { public static read(req: Request, res: express.Response, next: express.NextFunction) { let metrics = req.query.metrics.split(",").filter((item: string) => item !== ""); let dimensions = req.query.dimensions.split(",").filter((item: string) => item !== ""); + let clauses = []; + if (req.query.filters) { + clauses = req.query.filters.split(";").filter((item: string) => item !== ""); + } let view; try { - let query: Query = { metrics: [], dimensions: [] }; + let query: Query = { metrics: [], dimensions: [], clauses: [] }; for (let i = 0; i < metrics.length; ++i) { query.metrics.push(req.engine.getMetricByName(metrics[i])); } @@ -37,6 +41,10 @@ export class DataCtrl { for (let i = 0; i < dimensions.length; ++i) { query.dimensions.push(req.engine.getDimensionByName(dimensions[i])); } + + for (let i = 0; i < clauses.length; ++i) { + query.clauses.push(req.engine.parseClause(clauses[i])); + } view = req.engine.query(query); } catch (e) { diff --git a/src/common/query.ts b/src/common/query.ts index e56ff85b9bbcb67cdd2abf0b89c710fbc6bf4b73..dcbcb1f24cbec0d4ef85b45859facf519f85c4f8 100644 --- a/src/common/query.ts +++ b/src/common/query.ts @@ -20,9 +20,10 @@ import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; +import { Clause } from "../core/clause"; export interface Query { public metrics: Metric[]; public dimensions: Dimension[]; - + public clauses?: Clause[]; } diff --git a/src/core/clause.ts b/src/core/clause.ts new file mode 100644 index 0000000000000000000000000000000000000000..11a831dc55cc90349305958189d17d79175cecb0 --- /dev/null +++ b/src/core/clause.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 { Filter } from "./filter"; +import { Hash } from "../util/hash"; + +export interface ClauseOptions { + filters: Filter[]; +} + +export class Clause { + public readonly id: string; + public readonly filters: Filter[]; + + constructor (options: ClauseOptions) { + this.filters = options.filters; + const filtersIds = this.filters.map((item) => item.id); + this.id = Hash.sha1(filtersIds.sort()); + } + +} diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index cefbf13d104a7db4be78c4939300fefb146fb26f..00ad8be38c2f1bb66950dd2143e295ba5f4ccc39 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -23,6 +23,7 @@ import { expect } from "chai"; import { Engine } from "./engine"; import { Metric } from "./metric"; import { Dimension } from "./dimension"; +import { FilterOperator } from "./filter"; import { View } from "./view"; import { engineScenario } from "../../test/scenario"; @@ -182,4 +183,91 @@ describe("engine class", () => { } expect(error).to.be.true; }); + + it("should parse a clause, with target as dimension and operator as ==", () => { + const strFilter = "dim:0==0"; + const clause = engine.parseClause(strFilter); + expect(clause).to.be.an("object"); + expect(clause).to.have.property("filters"); + expect(clause).to.have.property("id"); + expect(clause.filters).to.be.an("array"); + expect(clause.filters).to.have.length(1); + expect(clause.filters[0]).to.have.property("id"); + expect(clause.filters[0]).to.have.property("target"); + expect(clause.filters[0]).to.have.property("value"); + expect(clause.filters[0]).to.have.property("operator"); + expect(clause.filters[0].target).to.be.equal(dim[0]); + expect(clause.filters[0].value).to.be.equal("0"); + expect(clause.filters[0].operator).to.be.equal(FilterOperator.EQUAL); + }); + it("should parse a clause, with target as metric and operator as !=", () => { + const strFilter = "met:0!=0"; + const clause = engine.parseClause(strFilter); + expect(clause).to.be.an("object"); + expect(clause).to.have.property("filters"); + expect(clause).to.have.property("id"); + expect(clause.filters).to.be.an("array"); + expect(clause.filters).to.have.length(1); + expect(clause.filters[0]).to.have.property("id"); + expect(clause.filters[0]).to.have.property("target"); + expect(clause.filters[0]).to.have.property("operator"); + expect(clause.filters[0]).to.have.property("value"); + expect(clause.filters[0].target).to.be.equal(met[0]); + expect(clause.filters[0].value).to.be.equal("0"); + expect(clause.filters[0].operator).to.be.equal(FilterOperator.NOTEQUAL); + }); + it("should throw an exception, when a dimension is not found", () => { + let error: boolean = false; + const strFilter = "dim:-1==0"; + const exeption = "Filter could not be created: \"dim:-1\" was not found"; + try { + engine.parseClause(strFilter); + } + catch (e){ + error = true; + expect(e.message).to.be.equal(exeption); + + } + expect(error).to.be.true; + }); + it("should throw an exception, when a metric is not found", () => { + let error: boolean = false; + const strFilter = "met:-1==0"; + const exeption = "Filter could not be created: \"met:-1\" was not found"; + try { + engine.parseClause(strFilter); + } + catch (e){ + error = true; + expect(e.message).to.be.equal(exeption); + + } + expect(error).to.be.true; + }); + it("should throw an exception, when a operator is not found", () => { + let error: boolean = false; + let strFilter = "met:-1=?0"; + let exeption = "Filter could not be created: Operator on \"" + strFilter + "\" could not be extracted"; + try { + engine.parseClause(strFilter); + } + catch (e){ + error = true; + expect(e.message).to.be.equal(exeption); + + } + expect(error).to.be.true; + error = false; + strFilter = "met:-1!?0"; + exeption = "Filter could not be created: Operator on \"" + strFilter + "\" could not be extracted"; + try { + engine.parseClause(strFilter); + } + catch (e){ + error = true; + expect(e.message).to.be.equal(exeption); + + } + expect(error).to.be.true; + }); }); diff --git a/src/core/engine.ts b/src/core/engine.ts index b4684354dcee877d9908c7ececda68db423b599e..44fcf68eb7353bc8cefa4bd87153c2f8c63dbccb 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -20,6 +20,8 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; +import { Clause } from "./clause"; +import { Filter } from "./filter"; import { View } from "./view"; import { Query } from "../common/query"; import { Graph } from "../util/graph"; @@ -88,12 +90,56 @@ export class Engine { return result; } + public parseClause(strClause: string): Clause { + let strFilters = strClause.split(",").filter((item: string) => item !== ""); + let filters: Filter[] = []; + for (let i = 0; i < strFilters.length; ++i) { + filters.push(this.parseFilter(strFilters[i])); + } + + return new Clause({filters: filters}); + } + + public parseFilter(strFilter: string): Filter { + let segment = Filter.segment(strFilter); + if (segment) { + // Segment never returns NONE + let op = Filter.parseOperator(segment.operator); + let target: Metric|Dimension = null; + try { + target = this.getDimensionByName(segment.target); + } + + catch (e) { + try { + target = this.getMetricByName(segment.target); + } + + catch (e) { + target = null; + } + } + + if (!target) { + throw new Error("Filter could not be created: \"" + segment.target + "\" was not found"); + } + + return new Filter({ + target: target, + operator: op, + value: segment.value + }); + } + else { + throw new Error("Filter could not be created: Operator on \"" + strFilter + "\" could not be extracted"); + } + } public query (q: Query) { return this.selectOptimalView(q); } private selectOptimalView (q: Query) { - let optimalViews = this.graph.cover(q.metrics, q.dimensions); + let optimalViews = this.graph.cover(q); if (optimalViews.length === 0) { throw new Error ("Engine views cannot cover the query"); } @@ -111,6 +157,7 @@ export class Engine { let options = { metrics: q.metrics, dimensions: q.dimensions, + clauses: ((q.clauses) ? q.clauses : []), materialized: false, origin: false, // Never a dynamic generated view will be origin childViews: optimalViews diff --git a/src/core/filter.spec.ts b/src/core/filter.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e109d2352318cacb616025b0b062d77074c03959 --- /dev/null +++ b/src/core/filter.spec.ts @@ -0,0 +1,31 @@ +/* + * 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 { FilterOperator, Filter } from "./filter"; + +describe("filter class", () => { + it("should correctly parse the operators", () => { + expect(Filter.parseOperator("==")).to.be.equal(FilterOperator.EQUAL); + expect(Filter.parseOperator("!=")).to.be.equal(FilterOperator.NOTEQUAL); + expect(Filter.parseOperator("?=")).to.be.equal(FilterOperator.NONE); + }); +}); diff --git a/src/core/filter.ts b/src/core/filter.ts new file mode 100644 index 0000000000000000000000000000000000000000..cffa555af42aa8939bf51ff82e1f0b96dfc0992b --- /dev/null +++ b/src/core/filter.ts @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 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 { Dimension } from "./dimension"; +import { Metric } from "./metric"; +import { Hash } from "../util/hash"; + +export interface FilterOptions { + target: Metric|Dimension; + operator: FilterOperator; + value: string; +} + +export interface StrFilterOptions { + target: string; + operator: string; + value: string; +} + +export enum FilterOperator { + NONE, + EQUAL, + NOTEQUAL +} + +export class Filter { + public readonly id: string; + public readonly target: Metric|Dimension; + public readonly operator: FilterOperator; + public readonly value: string; + + constructor (options: FilterOptions) { + this.target = options.target; + this.operator = options.operator; + this.value = options.value; + this.id = Hash.sha1(options.target.name + options.operator + options.value); + } + + public static parseOperator(op: string): FilterOperator { + switch (op) { + case "==": + return FilterOperator.EQUAL; + case "!=": + return FilterOperator.NOTEQUAL; + default: + return FilterOperator.NONE; + } + } + + public static segment(strFilter: string): StrFilterOptions { + for (let i = 0; i < strFilter.length; ++i) { + if (strFilter[i] === "=") { + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: "==", + value: strFilter.slice(i + 2) + }; + } + + } + + if (strFilter[i] === "!") { + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: "!=", + value: strFilter.slice(i + 2) + }; + } + + } + } + + return null; + } + +} diff --git a/src/core/view.ts b/src/core/view.ts index 8d1f9545cf566a958c8e7c7c3dc47dba13718126..ef83f082c68f46bdf3a9f633127163b083416dfa 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -21,6 +21,7 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; import { Hash } from "../util/hash"; +import { Clause } from "./clause"; export interface LoadView { view: View; @@ -31,6 +32,7 @@ export interface ViewOptions { metrics: Metric[]; dimensions: Dimension[]; origin: boolean; + clauses?: Clause[]; materialized?: boolean; childViews?: View[]; } @@ -39,6 +41,7 @@ export class View { public readonly id: string; public readonly metrics: Metric[]; public readonly dimensions: Dimension[]; + public readonly clauses: Clause[]; public readonly materialized: boolean; public readonly origin: boolean; public childViews: View[]; @@ -46,13 +49,16 @@ export class View { constructor (options: ViewOptions) { this.metrics = options.metrics.sort(); this.dimensions = options.dimensions.sort(); + this.clauses = (options.clauses) ? options.clauses.sort() : []; this.materialized = options.materialized || false; this.origin = options.origin || false; this.childViews = (options.childViews) ? options.childViews : []; // calculate the id of the view based on it's metrics and dimensions - let metricsNames = options.metrics.map(metric => metric.name); - let dimensionsNames = options.dimensions.map(dimension => dimension.name); - this.id = Hash.sha1(metricsNames.concat(dimensionsNames).sort()); + const metNames = this.metrics.map((metric) => metric.name); + const dimNames = this.dimensions.map((dimension) => dimension.name); + const clausesIds = this.clauses.map((clause) => clause.id); + const join = metNames.concat(dimNames).concat(clausesIds).sort(); + this.id = Hash.sha1(join); } } diff --git a/src/util/graph.spec.ts b/src/util/graph.spec.ts index 268011f60260d7e769d0a189f41966d0ce513dca..aab53671b40c8425fdfa03a30f1435180b2db2ee 100644 --- a/src/util/graph.spec.ts +++ b/src/util/graph.spec.ts @@ -23,8 +23,11 @@ import { expect } from "chai"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; import { View } from "../core/view"; +import { Filter, FilterOperator } from "../core/filter"; +import { Clause } from "../core/clause"; import { Graph } from "./graph"; import { AggregationType, RelationType } from "../common/types"; +import { Query } from "../common/query"; describe("graph class", () => { @@ -197,7 +200,8 @@ describe("graph class", () => { }); expect(g.addView(view)).to.be.true; - let children = g.cover([], [dim]); + const query: Query = { metrics: [], dimensions: [dim] }; + let children = g.cover(query); expect(children).to.be.an("array"); expect(children).to.have.length(1); expect(children[0].id).to.be.equal(view.id); @@ -266,7 +270,8 @@ describe("graph class", () => { expect(g.addView(views[i])).to.be.true; } - let children = g.cover([mets[0], mets[1]], [dims[0], dims[1]]); + const query: Query = { metrics: [mets[0], mets[1]], dimensions: [dims[0], dims[1]] }; + let children = g.cover(query); expect(children).to.be.an("array"); expect(children).to.have.length(1); expect(children[0].id).to.be.equal(views[views.length - 1].id); @@ -305,7 +310,8 @@ describe("graph class", () => { expect(g.addView(view)).to.be.true; - let children = g.cover([], [dims[1], dims[2]]); + const query: Query = { metrics: [], dimensions: [dims[1], dims[2]] }; + let children = g.cover(query); expect(children).to.be.an("array"); expect(children).to.have.length(1); expect(children[0].id).to.be.equal(view.id); @@ -344,8 +350,70 @@ describe("graph class", () => { expect(g.addView(view)).to.be.true; - let children = g.cover([], []); + const query: Query = { metrics: [], dimensions: [] }; + let children = g.cover(query); expect(children).to.be.an("array"); expect(children).to.be.empty; }); + + it("should cover the graph, even when a constraint edge can not be used", () => { + let dims = [ + new Dimension({name: "dim:0", dataType: "date"}), + new Dimension({name: "dim:1", dataType: "date"}), + new Dimension({name: "dim:2", dataType: "date"}), + ]; + + let filter1 = new Filter({ + target: dims[0], + operator: FilterOperator.EQUAL, + value: "01/01/01" + }); + let filter2 = new Filter({ + target: dims[0], + operator: FilterOperator.EQUAL, + value: "01/01/02" + }); + let clause1 = new Clause({filters: [filter1]}); + let clause2 = new Clause({filters: [filter2]}); + + let g = new Graph(); + + for (let i = 0; i < 3; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + } + + let view1 = new View({ + metrics: [], + dimensions: [dims[0], dims[1]], + origin: true, + materialized: true + }); + + let view2 = new View({ + metrics: [], + dimensions: [dims[1], dims[2]], + origin: true, + materialized: true + }); + + let view3 = new View({ + metrics: [], + dimensions: dims, + origin: false, + materialized: false, + childViews: [view1, view2], + clauses: [clause1] + }); + + expect(g.addView(view1)).to.be.true; + expect(g.addView(view2)).to.be.true; + expect(g.addView(view3)).to.be.true; + + const query: Query = { metrics: [], dimensions: dims, clauses: [clause2] }; + let children = g.cover(query); + expect(children).to.be.an("array"); + expect(children).to.have.length(2); + expect(children.every((item) => item.id !== view3.id)).to.be.true; + }); + }); diff --git a/src/util/graph.ts b/src/util/graph.ts index da49807dc1ed500c6722306e359e81f528b9280c..881ce7df1f85a1955860cb1ac7d268be6e81cc42 100644 --- a/src/util/graph.ts +++ b/src/util/graph.ts @@ -21,6 +21,8 @@ import { View } from "../core/view"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; +import { Query } from "../common/query"; +import { Clause } from "../core/clause"; enum State { UNVISITED, @@ -305,7 +307,10 @@ export class Graph { If this set cannot be created, throws a error */ - public cover(metrics: Metric[], dimensions: Dimension[]): View[] { + public cover(q: Query): View[] { + const metrics = q.metrics; + const dimensions = q.dimensions; + const clauses = (q.clauses) ? q.clauses : []; let output: View[] = []; let verticesIds = metrics.map((met) => met.name); verticesIds = verticesIds.concat(dimensions.map((dim) => dim.name)); @@ -335,7 +340,7 @@ export class Graph { let v: Vertex = queue.shift(); for (let key in v.neighbors) { let u: Vertex = this.verticeMap[key]; - if (u.state === State.UNVISITED) { + if (this.canVisit(u, v.neighbors[key], clauses)) { // Mark all vertices visited by the search u.state = State.VISITED; u.parent = v; @@ -369,9 +374,11 @@ export class Graph { let v = this.verticeMap[verticesIds.shift()]; while (v.parent !== null) { let options = v.parent.neighbors[v.id].filter((edge) => { - // Filther the edges ids to get only the ones that - // represent views - return edge.isView; + // Filter the edges ids to get only the ones that + // represent views, also get only the ones that pass + // in the constraints + return edge.isView && + this.passConstraints(clauses, edge.view.clauses); }).map((edge) => edge.view); // Check if there is a intersection between output and options if (output.some((child) => options.some((view) => child === view))) { @@ -486,4 +493,48 @@ export class Graph { return bestView; } + /* + Check if a vertex is can be visited from another vertex. + Basically checks if the vertex is unvisited, if is a sub dimension + or view edge and if is a view, chech the constraints. + */ + private canVisit(v: Vertex, neighbors: Edge[], clauses: Clause[] ): boolean { + if (v.state !== State.UNVISITED) { + return false; + } + + for (let i = 0; i < neighbors.length; ++i) { + if (neighbors[i].isView) { + if (this.passConstraints(clauses, neighbors[i].view.clauses)) { + return true; + } + } + + else { + return true; + } + } + + return false; + } + + /* + Check if a set of filter/clauses of a view suits for the query + */ + private passConstraints(constraints: Clause[], target: Clause[]) { + /* + TODO: Enhance constraint check. + + The assumption is that the constraints are simple now and only + check sub set is enought, but if the clauses get more complex + then this function should be updated. + + Remember: The fact that a view cannot be choosed by constraints + only means that a more inneficient view must be choosen. + */ + + return target.every((item) => constraints.some((c) => c.id === item.id)); + + } + } diff --git a/test/scenario.ts b/test/scenario.ts index 0a896280eccbcda089f14eb8a13e6e6ee6b39acf..04861a044df7ec3f7c4cad1ad7001aa9fb4b681a 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -22,6 +22,8 @@ import { ConfigParser } from "../src/util/configParser"; import { Metric } from "../src/core/metric"; import { Dimension } from "../src/core/dimension"; import { View } from "../src/core/view"; +import { Filter, FilterOperator } from "../src/core/filter"; +import { Clause } from "../src/core/clause"; import { AggregationType, RelationType } from "../src/common/types"; import { Query} from "../src/common/query"; @@ -42,12 +44,17 @@ interface AdapterScenario { join4View: View; dateView: View; aggrView: View; + clauseView: View; + multiFilterView: View; + multiClauseView: View; + notEqualView: View; } interface DataCtrlScenario { wrongMet: Query; wrongDim: Query; correct: Query; + clausal: Query; } const config = ConfigParser.parse("config/test.yaml"); @@ -56,6 +63,35 @@ const mets = config.metrics; const dims = config.dimensions; const views = config.views; +const filters: { [key: string]: Filter } = { + "dim:0:0" : new Filter({ + target: dims[0], + operator: FilterOperator.EQUAL, + value: "2017-01-02" + }), + "dim:0:1" : new Filter({ + target: dims[0], + operator: FilterOperator.EQUAL, + value: "2017-01-03" + }), + "dim:3" : new Filter({ + target: dims[3], + operator: FilterOperator.NOTEQUAL, + value: "dim:3:1" + }), + "dim:7" : new Filter({ + target: dims[7], + operator: FilterOperator.EQUAL, + value: "1" + }) +}; + +const clauses: { [key: string]: Clause } = { + "view0dim7": new Clause({filters: [filters["dim:7"]]}), + "view0dim0": new Clause({filters: [filters["dim:0:0"], filters["dim:0:1"]]}), + "view3dim3": new Clause({filters: [filters["dim:3"]]}) +}; + const wrongMet = new Metric({ name: "met:11", aggregation: AggregationType.COUNT, @@ -141,6 +177,42 @@ const aggrView = new View({ childViews: [views[0], views[2], views[4]] }); +const clauseView = new View({ + metrics: [mets[0], mets[1], mets[2]], + dimensions: [dims[0]], + materialized: false, + origin: false, + childViews: [views[0]], + clauses: [clauses.view0dim7] +}); + +const multiFilterView = new View({ + metrics: [mets[0], mets[1]], + dimensions: [dims[0]], + materialized: false, + origin: false, + childViews: [views[0]], + clauses: [clauses.view0dim0] +}); + +const multiClauseView = new View({ + metrics: [mets[0], mets[1]], + dimensions: [dims[0]], + materialized: false, + origin: false, + childViews: [views[0]], + clauses: [clauses.view0dim0, clauses.view0dim7] +}); + +const notEqualView = new View({ + metrics: [], + dimensions: [dims[3]], + materialized: false, + origin: false, + childViews: [views[3]], + clauses: [clauses.view3dim3] +}); + const subDimView = new View({ metrics: [mets[0]], dimensions: [subdims[0], subdims[1], dims[7], dims[8]], @@ -189,11 +261,16 @@ export const adapterScenario: AdapterScenario = { subDimensionView: subDimView, join4View: join4View, dateView: dateView, - aggrView: aggrView + aggrView: aggrView, + clauseView: clauseView, + multiFilterView: multiFilterView, + multiClauseView: multiClauseView, + notEqualView: notEqualView }; export const dataCtrlScenario: DataCtrlScenario = { wrongMet: { metrics: [wrongMet], dimensions: [dims[0]] }, wrongDim: { metrics: [mets[0]], dimensions: [wrongDim] }, - correct: { metrics: [mets[0]], dimensions: [dims[0]] } + correct: { metrics: [mets[0]], dimensions: [dims[0]] }, + clausal: { metrics: [mets[0]], dimensions: [dims[7]] } };