diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0748346158196aac0e7bd9e9503d70fd4247287..893848c6b8fe1ec7e2c1a9df780bf9095f5d9e24 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,13 @@ image: node:6.2 +services: + - postgres:latest + +variables: + POSTGRES_DB: 'blendb_fixture' + POSTGRES_USER: 'runner' + POSTGRES_PASSWORD: '' + cache: paths: - node_modules @@ -9,6 +17,8 @@ before_script: run_tests: script: + - mv config/ci_test.yaml.example config/test.yaml - npm test tags: - node + - postgres diff --git a/config/ci_test.yaml.example b/config/ci_test.yaml.example new file mode 100644 index 0000000000000000000000000000000000000000..2c49af1a1850ca344038e4505444fb91cb13597e --- /dev/null +++ b/config/ci_test.yaml.example @@ -0,0 +1,176 @@ +# gitignore ignores files.yaml in this folder +# however a config file for tests in CI is required +# so this example file in fact is the CI test file +connection: + database: 'blendb_fixture' + user: 'runner' + password: '' + host: 'postgres' + port: 5432 + max: 10 + idleTimeoutMillis: 30000 +struct: + create: true + insert: true +schema: + views: + - + alias: "View 1" + data: "test/postgres/fixtures/view1.json" + dimensions: + - "dim:1" + - "dim:2" + metrics: + - "met:1" + - "met:2" + - "met:3" + - + alias: "View 2" + data: "test/postgres/fixtures/view2.json" + dimensions: + - "dim:1" + - "dim:2" + metrics: + - "met:1" + - "met:3" + - "met:5" + - + alias: "View 3" + data: "test/postgres/fixtures/view3.json" + dimensions: + - "dim:4" + - "dim:5" + metrics: + - "met:3" + - "met:4" + - "met:7" + - + alias: "View 4" + data: "test/postgres/fixtures/view4.json" + dimensions: + - "dim:3" + - "dim:4" + - "dim:5" + - "dim:6" + metrics: + - "met:6" + - "met:7" + - + alias: "View 5" + data: "test/postgres/fixtures/view5.json" + dimensions: + - "dim:1" + - "dim:2" + - "dim:7" + metrics: + - "met:2" + - "met:3" + - "met:8" + - + alias: "View 6" + data: "test/postgres/fixtures/view6.json" + dimensions: + - "dim:1" + - "dim:2" + metrics: + - "met:1" + - "met:4" + - + alias: "View 7" + data: "test/postgres/fixtures/view7.json" + dimensions: + - "dim:8" + - "dim:9" + - "dim:10" + metrics: + - "met:8" + - + alias: "View 8" + data: "test/postgres/fixtures/view8.json" + dimensions: + - "dim:8" + - "dim:9" + - "dim:10" + metrics: + - "met:9" + - + alias: "View 9" + data: "test/postgres/fixtures/view9.json" + dimensions: + - "dim:8" + - "dim:9" + - "dim:10" + metrics: + - "met:10" + metrics: + - + name: "met:1" + dataType: "integer" + aggregation: "sum" + - + name: "met:2" + dataType: "integer" + aggregation: "avg" + - + name: "met:3" + dataType: "integer" + aggregation: "avg" + - + name: "met:4" + dataType: "integer" + aggregation: "sum" + - + name: "met:5" + dataType: "integer" + aggregation: "sum" + - + name: "met:6" + dataType: "integer" + aggregation: "avg" + - + name: "met:7" + dataType: "integer" + aggregation: "count" + - + name: "met:8" + dataType: "integer" + aggregation: "count" + - + name: "met:9" + dataType: "integer" + aggregation: "sum" + - + name: "met:10" + dataType: "integer" + aggregation: "count" + dimensions: + - + name: "dim:1" + dataType: "date" + - + name: "dim:2" + dataType: "date" + - + name: "dim:3" + dataType: "integer" + - + name: "dim:4" + dataType: "string" + - + name: "dim:5" + dataType: "string" + - + name: "dim:6" + dataType: "boolean" + - + name: "dim:7" + dataType: "integer" + - + name: "dim:8" + dataType: "integer" + - + name: "dim:9" + dataType: "date" + - + name: "dim:10" + dataType: "string" diff --git a/config/config.yaml.example b/config/config.yaml.example new file mode 100644 index 0000000000000000000000000000000000000000..f97ea97db5adb4daf21d7011aa78bbaa50e61803 --- /dev/null +++ b/config/config.yaml.example @@ -0,0 +1,15 @@ +connection: + user: 'blendb' + database: 'blendb-test' + password: 'secret' + host: 'localhost' + port: 5432 + max: 10 + idleTimeoutMillis: 30000 +struct: + create: false + insert: false +schema: + views: + metrics: + dimensions: diff --git a/database/config.ts b/database/config.ts index e138ee60c67a8de25fbd39ad05044050525337fc..5bf471c97ada9c60263c244d541c2483dd1bf208 100755 --- a/database/config.ts +++ b/database/config.ts @@ -83,7 +83,7 @@ function createView(view: View): string { let keys = []; for (let field in view.fields) { props.push("\"" + field + "\" " + typeConvertion(view.fields[field])); - keys.push(field); + keys.push(field.name); } keys.sort(); let name = "view_" + Hash.sha1(keys); diff --git a/package.json b/package.json index 488e010a664ce248e32a484514b241819625dcba..af5e12d36e5e1f62168eb4b034b36401b8c6003c 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,21 @@ "author": "Centro de Computação Científica e Software Livre (C3SL)", "license": "GPL-3.0", "dependencies": { + "@types/async": "^2.0.40", "@types/chai": "^3.4.33", "@types/d3": "^3.5.36", "@types/express": "^4.0.33", + "@types/js-yaml": "^3.5.29", "@types/mocha": "^2.2.32", + "@types/pg": "^6.1.38", "@types/pug": "^2.0.1", + "async": "^2.3.0", "express": "^4.0.33", + "js-yaml": "^3.8.2", "mississippi": "^1.2.0", "node-uuid": "^1.4.7", "osprey": "^0.3.2", + "pg": "^6.1.5", "pug": "^2.0.0-beta6", "ts-node": "^1.3.0", "typescript": "^2.0.3" diff --git a/src/adapter/postgres.spec.ts b/src/adapter/postgres.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..adc09ec8c04ed8e3fe27cbd4984439741264c6ee --- /dev/null +++ b/src/adapter/postgres.spec.ts @@ -0,0 +1,151 @@ +/* + * 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 { PostgresAdapter } from "./postgres"; +import { Adapter } from "../core/adapter"; +import { Fixture } from "../../test/postgres/fixture"; + +import { ConfigParser } from "../util/configParser"; +import { adapterScenario } from "../../test/scenario"; + +describe("postgres adapter", () => { + + // Initializing + let config: any; + let adapter: Adapter; + let fixture; + before((done) => { + config = ConfigParser.parse("config/test.yaml"); + fixture = new Fixture(config.connection); + fixture.load(config.loadViews, config.struct.create, (err) => { + if (err) { + throw err; + } + adapter = new PostgresAdapter(config.connection); + done(); + }); + }); + // Tests + it("should get data from single materialized view", (done) => { + let view = adapterScenario.materializedView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(5); + 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 join of 2 views (without selection)", (done) => { + let view = adapterScenario.noSelectionView; + 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); + }); + done(); + }); + }); + it("should get data from join of 2 views (with selection)", (done) => { + let view = adapterScenario.withSelectionView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(5); + 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 single view (with sub-dimension)", (done) => { + let view = adapterScenario.subDimensionView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(25); + 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 join of 4 views (with selection)", (done) => { + let view = adapterScenario.join4View; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(125); + 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 different sub dimensions with same parent", (done) => { + let view = adapterScenario.dateView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(5); + 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(row).to.have.property("dim:2:year", 2017); + }); + done(); + }); + }); +}); diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts index 4c7e8b9996b7b05c219153dfe35e69eda626bd1e..7e133dd8393bb3ad24958fec79a09fd5328e1d29 100644 --- a/src/adapter/postgres.ts +++ b/src/adapter/postgres.ts @@ -19,136 +19,171 @@ */ import { Adapter } from "../core/adapter"; +import { Metric } from "../core/metric"; +import { Dimension } from "../core/dimension"; import { AggregationType, RelationType } from "../common/types"; import { View, ChildView } from "../core/view"; +import { Pool, PoolConfig } from "pg"; -interface ParsedView { +interface ParsedChild { query: string; view: View; + dimensions: Dimension[]; + metrics: Metric[]; + alias: string; }; -export class PostgresAdapter extends Adapter{ - public getDataFromView(view: View): string { +export class PostgresAdapter extends Adapter { + private pool: Pool; + + constructor (config: PoolConfig) { + super(); + 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 - return this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n"; + let query = this.buildQueryFromView(view, view.metrics, view.dimensions) + ";\n"; + this.pool.connect((err, client, done) => { + if (err) { + cb (err); + } + + client.query(query, [], (error, result) => { + // call 'done()' to release client back to pool + done(); + cb(error, (result) ? result.rows : null); + }); + }); } - public materializeView(view: View): string { - return null; + public materializeView(view: View): boolean { + return false; } - private buildQueryFromView (view: View, metrics: Metric[], dimensions: Dimession[]): string { + private buildQueryFromView (view: View, metrics: Metric[], dimensions: Dimension[]): string { /* Reduce metrics and dimensions array to the intersection with the view. So is possible only get useful data in the sub-querys. */ let strMetrics = metrics.map((metric) => { let func = this.getAggregateFunction(metric.aggregation); - let extMetric = func + "(" + metric.name + ")"; + let quotedName = "\"" + metric.name + "\""; + let extMetric = func + "(" + quotedName + ") AS " + quotedName; return extMetric; }); if (view.materialized) { - let strDimensions = dimensions.map((dimmension) => dimension.name ); - let sql = "(SELECT " + strMetrics.join(", ") + ", " + strDimensions.join(", "); + let strDimensions = dimensions.map((dimension) => "\"" + dimension.name + "\""); + let sql = "(SELECT " + strMetrics.concat(strDimensions).join(", "); sql += " FROM " + "view_" + view.id; - sql += " GROUP BY " + strDimensions.join(", "); - sql += ")\n"; + if (strDimensions.length > 0 && strMetrics.length > 0) { + sql += " GROUP BY " + strDimensions.join(", "); + } + sql += ")"; return sql; } else { - let children: ParsedView[] = view.childViews.map((item: ChildView) => { - return { - query: this.buildQueryFromView(item.view, item.metrics, item.dimensions), - view: item.view - }; - }); let covered = new Map(); - dimensions.forEach((item) => covered.set(item.name, "")); + let matchable: any[] = [] ; + dimensions.forEach((item) => { + covered.set(item.name, ""); + /* + For each dimension that must be covered + create a match in the array. + If a more than onde view match to the same + dimension, cretes a WHERE clause + */ + matchable.push({match: item.name, sub: item}); + let dim = item; + /* + Sub dimensions also have parents that can match + with then too. + */ + while (dim.relation !== RelationType.NONE) { + dim = dim.parent; + matchable.push({ + match: dim.name, + sub: item, + }); + } + }); metrics.forEach((item) => covered.set(item.name, "")); - let projection = "SELECT "; - let viewsFrom = "FROM "; - let selection = "WHERE "; - let grouping = "GROUP BY "; - - let elements = []; - let group = []; + let elements: string[] = []; + let group: string[] = []; + let viewsQuery: string[] = []; + let selected: string[] = []; - children.forEach((child: ParsedView) => { - let selected = []; - let matchable = child.dimensions.map((item) => { - return { match: item, sub: item, viewId: child.view.id }; + let children: ParsedChild[] = view.childViews.map((item: ChildView) => { + let dims = item.view.dimensions.filter((dim) => { + return matchable.some((match) => match.match === dim.name); }); - // Running over the dimensions that cover some part of the query - child.dimensions.forEach((dimension: Dimension) => { - /* - If a dimension has a parent, the parent must match with - the sub dimension, so must be added in the matchable - array, for the selection (WHERE statement). - */ - let dim = dimension; - while (dim.relation !== RelationType.NONE) { - matchable.push({ - match: dim.name, - sub: dimension, - viewId: child.view.id - }); - dim = dim.parent; - } - - dim = dimension; - let extDimension = "view_" + child.view.id + ".\"" + dim.name + "\""; - - // If the view dimension does not match, search for parent - while (!child.dimensions.some((item) => item.name === dim.name)) { - dim = dim.parent; - extDimension = this.translateRelation(dim.relation, extDimension); - } - covered.set(dimension.name, extDimension); - elements.push(extDimension + " AS " + dimension.name); - group.push(dimension.name); + let mets = item.metrics.filter((met) => { + return metrics.some((elem) => elem.name === met.name); }); + let query = ""; + if (dims.length !== 0 || mets.length !== 0) { + query = this.buildQueryFromView(item.view, mets, dims); + } + return { + query: query, + view: item.view, + dimensions : dims, + metrics: mets, + alias: "alias_" + item.view.id + }; + }).filter ((item) => item.query !== ""); + + children.forEach((child: ParsedChild) => { child.view.dimensions.forEach((dimension: Dimension) => { /* Make selection. Search for dimensions, that are in matchable array. */ matchable.filter((item) => { - return item.viewId !== child.view.id && - item.match === dimension.name; + return item.match === dimension.name; }) .forEach((item) => { // Expand the sub-dimension until match with a parent let dim = item.sub; - let extDimension = "view_" + child.view.id + ".\"" + dimension.name + "\""; + let extDimension = child.alias + ".\"" + dimension.name + "\""; while (dim.name !== item.match) { + extDimension = this.translateRelation(dim.relation, extDimension); dim = dim.parent; - extDimension = this.translateRelation(dim, extDimension); } - selected.push(extDimension + " = " + covered.get(item.sub.name)); + + if (covered.get(item.sub.name) === "") { + elements.push(extDimension + " AS \"" + item.sub.name + "\""); + covered.set(item.sub.name, extDimension); + group.push(extDimension); + } + else { + selected.push(extDimension + " = " + covered.get(item.sub.name)); + } }); }); child.metrics.forEach((metric: Metric) => { - let func = this.geAggregateFunction(metric.aggregation); - let extMetric = func + "(view_" + child.view.id + "." + metric.name + ")"; + let func = this.getAggregateFunction(metric.aggregation); + let quotedName = "\"" + metric.name + "\""; + let extMetric = func + "(" + child.alias + "." + quotedName + ") AS " + quotedName; elements.push(extMetric); }); - viewsFrom += "\n" + child.query; + viewsQuery.push(child.query + " AS " + child.alias); - if (selected.length > 0) { - selection += selected.join(" AND "); - } }); - projection += elements.join(", ") + "\n"; - selection += "\n"; - grouping += group.join(", ") + "\n"; + let projection = "SELECT " + elements.join(", ") + "\n"; + let viewsFrom = "FROM " + viewsQuery.join(", ") + "\n"; + let selection = (selected.length > 0) ? "WHERE " + selected.join(" AND ") + "\n" : ""; + let grouping = ""; + if (group.length > 0 && metrics.length > 0) { + grouping = "GROUP BY " + group.join(", ") + "\n"; + } return "(" + projection + viewsFrom + selection + grouping + ")"; } @@ -184,7 +219,7 @@ export class PostgresAdapter extends Adapter{ } - private applyRelation(name, args, values): string { + 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 diff --git a/src/core/adapter.ts b/src/core/adapter.ts index f2981953b9f362da9e07cdab004454865facc304..20cb90cfa5b995959f0aa7fb6b1d9381d97e0cdf 100644 --- a/src/core/adapter.ts +++ b/src/core/adapter.ts @@ -21,6 +21,6 @@ import { View } from "./view"; export abstract class Adapter { - public abstract getDataFromView(view: View): string; - public abstract materializeView(view: View): string; + public abstract getDataFromView(view: View, cb: (err: Error, result: any[]) => void): void; + public abstract materializeView(view: View): boolean; } diff --git a/src/core/dimension.ts b/src/core/dimension.ts index 386d0b5011c84f35b1189b771a1f2be1e31e6181..bf75265a66839a5dafc0385f0d74ca4c989c45b0 100644 --- a/src/core/dimension.ts +++ b/src/core/dimension.ts @@ -22,17 +22,20 @@ import { RelationType } from "../common/types"; export interface DimensionOptions { name: string; + dataType: string; parent?: Dimension; relation?: RelationType; } export class Dimension { public readonly name: string; + public readonly dataType: string; public readonly parent: Dimension; public readonly relation: RelationType; constructor(options: DimensionOptions) { this.name = options.name; + this.dataType = options.dataType; this.relation = (options.relation) ? options.relation : RelationType.NONE; this.parent = (options.parent) ? options.parent : null; } diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index fc954355b1182d4cc5bf088c8d8ecb4ec097bb92..13f64fcba079781958e8aa3a171572e37be8b581 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -24,120 +24,30 @@ import { Engine } from "./engine"; import { Metric } from "./metric"; import { Dimension } from "./dimension"; import { View } from "./view"; -import { AggregationType } from "../common/types"; -import { RelationType } from "../common/types"; +import { engineScenario } from "../../test/scenario"; describe("engine class", () => { const engine = new Engine(); - const met1 = new Metric({ name: "met:1", aggregation: AggregationType.SUM }); - const met2 = new Metric({ name: "met:2", aggregation: AggregationType.AVG }); - const met3 = new Metric({ name: "met:3", aggregation: AggregationType.AVG }); - const met4 = new Metric({ name: "met:4", aggregation: AggregationType.SUM }); - const met5 = new Metric({ name: "met:5", aggregation: AggregationType.SUM }); - const met6 = new Metric({ name: "met:6", aggregation: AggregationType.AVG }); - const met7 = new Metric({ name: "met:7", aggregation: AggregationType.COUNT }); - const met8 = new Metric({ name: "met:8", aggregation: AggregationType.COUNT }); - const met9 = new Metric({ name: "met:9", aggregation: AggregationType.SUM }); - const met10 = new Metric({ name: "met:10", aggregation: AggregationType.COUNT }); - const met11 = new Metric({ name: "met:11", aggregation: AggregationType.COUNT }); + const met = engineScenario.metrics; + const dim = engineScenario.dimensions; + const subdim = engineScenario.subDimensions; + const views = engineScenario.views; - const dim1 = new Dimension({ name: "dim:1" }); - const dim2 = new Dimension({ name: "dim:2" }); - const dim3 = new Dimension({ name: "dim:3" }); - const dim4 = new Dimension({ name: "dim:4" }); - const dim5 = new Dimension({ name: "dim:5" }); - const dim6 = new Dimension({ name: "dim:6" }); - const dim7 = new Dimension({ name: "dim:7" }); - const dim8 = new Dimension({ name: "dim:8" }); - const dim9 = new Dimension({ name: "dim:9" }); - const dim10 = new Dimension({ name: "dim:10" }); - const dim11 = new Dimension({ name: "dim:11" }); - - const subdim1 = new Dimension({ name: "sub:1", parent: dim1, relation: RelationType.DAY }); - const subdim2 = new Dimension({ name: "sub:2", parent: dim9, relation: RelationType.DAY }); - const subdim3 = new Dimension({ name: "sub:3", parent: subdim1, relation: RelationType.DAY }); - const subdim4 = new Dimension({ name: "sub:4", parent: null, relation: RelationType.DAY }); - const subdim5 = new Dimension({ name: "sub:5", parent: dim2, relation: RelationType.DAY }); - - engine.addMetric(met1); - engine.addMetric(met2); - engine.addMetric(met3); - engine.addMetric(met4); - engine.addMetric(met5); - engine.addMetric(met6); - engine.addMetric(met7); - engine.addMetric(met8); - engine.addMetric(met9); - engine.addMetric(met10); - - engine.addDimension(dim1); - engine.addDimension(dim2); - engine.addDimension(dim3); - engine.addDimension(dim4); - engine.addDimension(dim5); - engine.addDimension(dim6); - engine.addDimension(dim7); - engine.addDimension(dim8); - 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]}), - new View({ metrics: [met3, met4, met7], dimensions: [dim4, dim5]}), - new View({ metrics: [met6, met7], dimensions: [dim3, dim4, dim5, dim6]}), - new View({ metrics: [met8, met2, met3], dimensions: [dim1, dim2, dim7]}), - new View({ metrics: [met2, met4], dimensions: [dim1, dim2]}), - new View({ metrics: [met8], dimensions: [dim8, dim9, dim10]}), - new View({ metrics: [met9], dimensions: [dim8, dim9, dim10]}), - new View({ metrics: [met10], dimensions: [dim8, dim9, dim10]}) - ]; - - views.push(new View({ - metrics: [met1, met2, met3, met4, met5], - dimensions: [dim1, dim2], - materialized: false, - childViews: [ - { view: views[0], metrics: [met1, met2, met3], dimensions: [dim1, dim2]}, - { view: views[6], metrics: [met4], dimensions: []}, - { view: views[1], metrics: [met5], dimensions: []} - ] - })); - - views.push(new View({ - metrics: [met8, met9, met10], - dimensions: [dim8, dim9, dim10], - materialized: false, - childViews: [ - { view: views[7], metrics: [met8], dimensions: [dim8, dim9, dim10]}, - { view: views[8], metrics: [met9], dimensions: []}, - { view: views[9], metrics: [met10], dimensions: []} - ] - })); - - views.push(new View({ - metrics: [met1], - dimensions: [subdim1, subdim2], - materialized: false, - childViews: [ - { view: views[0], metrics: [met1], dimensions: [dim1]}, - { view: views[9], metrics: [], dimensions: [dim9]} - ] - })); + for (let i = 0; i < 10; ++i) { + engine.addMetric(met[i]); + engine.addDimension(dim[i]); + if (i < 5) { + engine.addDimension(subdim[i]); + } + } views.forEach((view) => engine.addView(view)); it("should be create a fill that cover the query and has 4 children", () => { let query = { - metrics : [met1, met2, met3, met4, met5, met6, met7, met8, met9, met10] - , dimensions : [dim1, dim2, dim3, dim4, dim5, dim6, dim7, dim8, dim9, dim10] + metrics : met.slice(0, 10) + , dimensions : dim.slice(0, 10) }; let optimalView = engine.query(query); expect(optimalView).to.be.an("object"); @@ -150,22 +60,22 @@ describe("engine class", () => { expect(optimalView.metrics).to.have.length(10); expect(optimalView.dimensions).to.have.length(10); expect(optimalView.childViews).to.have.length(4); - let metAux: number[] = optimalView.metrics.sort().map((met: Metric) => { - return Number(met.name.split(":")[1]); + let metAux: number[] = optimalView.metrics.sort().map((item: Metric) => { + return Number(item.name.split(":")[1]); }); - let dimAux: number[] = optimalView.dimensions.sort().map((dim: Dimension) => { - return Number(dim.name.split(":")[1]); + let dimAux: number[] = optimalView.dimensions.sort().map((item: Dimension) => { + return Number(item.name.split(":")[1]); }); for (let i: number = 0; i < 10; ++i) { - expect(dimAux[i]).to.be.equal(i+1); - expect(metAux[i]).to.be.equal(i+1); + expect(dimAux[i]).to.be.equal(i + 1); + expect(metAux[i]).to.be.equal(i + 1); } }); it("should throw an exception, query with non-existent metric", () => { let error: boolean = false; try { - engine.query({metrics: [met11], dimensions: [dim1]}); + engine.query({metrics: [met[10]], dimensions: [dim[0]]}); } catch (e){ error = true; @@ -178,7 +88,7 @@ describe("engine class", () => { it("should throw an exception, query with non-existent dimension", () => { let error: boolean = false; try { - engine.query({metrics: [met1], dimensions: [dim11]}); + engine.query({metrics: [met[0]], dimensions: [dim[10]]}); } catch (e){ error = true; @@ -190,8 +100,8 @@ describe("engine class", () => { 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] + metrics : met.slice(0, 3) + , dimensions : dim.slice(0, 2) }; let optimalView = engine.query(query); expect(optimalView).to.be.an("object"); @@ -212,7 +122,7 @@ describe("engine class", () => { let emptyMetrics: Metric[] = []; let query = { metrics : emptyMetrics - , dimensions : [subdim1, subdim2] + , dimensions : subdim.slice(0, 2) }; let optimalView = engine.query(query); expect(optimalView).to.be.an("object"); @@ -227,10 +137,10 @@ describe("engine class", () => { expect(optimalView.childViews).to.have.length(1); expect(optimalView).satisfy((optView: View) => { - return optView.childViews[0].dimensions.some((item) => item.name === subdim1.name); + 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 === subdim1.name); + return optView.childViews[0].dimensions.some((item) => item.name === subdim[0].name); }); }); @@ -238,7 +148,7 @@ describe("engine class", () => { let emptyMetrics: Metric[] = []; let query = { metrics : emptyMetrics - , dimensions : [subdim3, subdim5] + , dimensions : [subdim[2], subdim[4]] }; let optimalView = engine.query(query); expect(optimalView).to.be.an("object"); @@ -253,21 +163,21 @@ describe("engine class", () => { expect(optimalView.childViews).to.have.length(1); expect(optimalView).satisfy((optView: View) => { - return optView.childViews[0].dimensions.some((item) => item.name === subdim3.name); + return optView.childViews[0].dimensions.some((item) => item.name === subdim[2].name); }); expect(optimalView).satisfy((optView: View) => { - return optView.childViews[0].dimensions.some((item) => item.name === subdim5.name); + return optView.childViews[0].dimensions.some((item) => item.name === subdim[4].name); }); }); it("should throw an exception, sub-dimension with non-existent parent", () => { let error: boolean = false; try { - engine.query({metrics: [met11], dimensions: [subdim4]}); + engine.query({metrics: [met[10]], dimensions: [subdim[3]]}); } catch (e){ error = true; - expect(e.message).to.be.equal("Engine sub-dimention " + subdim4.name + " with no parent"); + expect(e.message).to.be.equal("Engine sub-dimention " + subdim[3].name + " with no parent"); } expect(error).to.be.true; diff --git a/src/core/metric.ts b/src/core/metric.ts index adbabeaf3f577ce51c6824709ed7279877b351d6..4f0d1c4579d2fc9a83c97ea2cbbccd4545f15f28 100644 --- a/src/core/metric.ts +++ b/src/core/metric.ts @@ -23,14 +23,17 @@ import { AggregationType } from "../common/types"; export interface MetricOptions { name: string; aggregation: AggregationType; + dataType: string; } export class Metric { public readonly name: string; public readonly aggregation: AggregationType; + public readonly dataType: string; constructor(options: MetricOptions) { this.name = options.name; this.aggregation = options.aggregation; + this.dataType = options.dataType; } } diff --git a/src/core/view.ts b/src/core/view.ts index 66fd11d447a87591610d1039f305565044cf895b..6de687e4cf7bf098e6568cf4b253cacd3cd9b8a5 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -22,6 +22,11 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; import { Hash } from "../util/hash"; +export interface LoadView { + view: View; + data: string; +} + export interface ChildView { metrics: Metric[]; dimensions: Dimension[]; @@ -45,8 +50,8 @@ export class View { constructor (options: ViewOptions) { this.metrics = options.metrics.sort(); this.dimensions = options.dimensions.sort(); - this.materialized = options.materialized || true; - this.childViews = options.childViews || []; + this.materialized = options.materialized || 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); diff --git a/src/util/configParser.spec.ts b/src/util/configParser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb13d67ab39a79550e372efecd5ba811014a18a0 --- /dev/null +++ b/src/util/configParser.spec.ts @@ -0,0 +1,70 @@ +/* + * 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 { ConfigParser, ViewParsingOptions } from "./configParser"; + +describe("configParser utility library", () => { + let metMap = new Map(); + let dimMap = new Map(); + it("should throw expection for inexistent metric", () => { + let opts: ViewParsingOptions = { + alias: "Test", + data: "test", + dimensions: [], + metrics: ["met:-1"] + }; + + let error: boolean = false; + try { + ConfigParser.parseViewOpt(opts, metMap, dimMap); + } + catch (e) { + error = true; + expect(e.message).to.be + .equal("[Parsing error] Non exist metric set to view " + opts.alias); + } + + expect(error).to.be.true; + }); + + it("should throw expection for inexistent metric", () => { + let opts: ViewParsingOptions = { + alias: "Test", + data: "test", + dimensions: ["dim:-1"], + metrics: [] + }; + + let error: boolean = false; + try { + ConfigParser.parseViewOpt(opts, metMap, dimMap); + } + catch (e) { + error = true; + expect(e.message).to.be + .equal("[Parsing error] Non exist dimension set to view " + opts.alias); + } + + expect(error).to.be.true; + }); + +}); diff --git a/src/util/configParser.ts b/src/util/configParser.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6490e0660417f3fe0a14a10dcfc1c7e3d7bf972 --- /dev/null +++ b/src/util/configParser.ts @@ -0,0 +1,137 @@ +/* + * 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 { Metric, MetricOptions } from "../core/metric"; +import { Dimension, DimensionOptions } from "../core/dimension"; +import { View, ViewOptions, LoadView } from "../core/view"; +import { PoolConfig } from "pg"; +import * as fs from "fs"; +import * as yaml from "js-yaml"; + +export interface ViewParsingOptions { + alias: string; + data: string; + dimensions: string[]; + metrics: string[]; +} + +interface ConfigSchema { + views: ViewParsingOptions[]; + metrics: MetricOptions[]; + dimensions: DimensionOptions[]; +} + +interface ConfigFile { + connection: PoolConfig; + struct: LoadStruct; + schema: ConfigSchema; +} + +export interface LoadStruct{ + create: boolean; + insert: boolean; +} + +export interface ParsedConfig { + connection: PoolConfig; + views: View[]; + metrics: Metric[]; + dimensions: Dimension[]; + struct: LoadStruct; + loadViews: LoadView[]; +} + +export class ConfigParser { + public static parse(configPath: string): ParsedConfig { + let config: ConfigFile = yaml.safeLoad(fs.readFileSync(configPath, { + encoding: "utf-8" + })); + + let metricsOpts = config.schema.metrics; + let viewsOpts = config.schema.views; + let dimensionsOpts = config.schema.dimensions; + let parsed: ParsedConfig = { + connection: config.connection, + views: [], + metrics: [], + dimensions: [], + struct: config.struct, + loadViews: [] + }; + + let metMap: Map<string, Metric> = new Map(); + let dimMap: Map<string, Dimension> = new Map(); + + for (let i = 0; i < metricsOpts.length; ++i) { + let met = new Metric(metricsOpts[i]); + parsed.metrics.push(met); + metMap.set(met.name, met); + } + + for (let i = 0; i < dimensionsOpts.length; ++i) { + let dim = new Dimension(dimensionsOpts[i]); + parsed.dimensions.push(dim); + dimMap.set(dim.name, dim); + } + + for (let i = 0; i < viewsOpts.length; ++i) { + let viewOpts = ConfigParser.parseViewOpt(viewsOpts[i], metMap, dimMap); + let view = new View(viewOpts); + parsed.views.push(view); + let loadView: LoadView = {view: view, data: viewsOpts[i].data}; + parsed.loadViews.push(loadView); + } + + return parsed; + } + + public static parseViewOpt(opts: ViewParsingOptions, + metMap: Map<string, Metric>, + dimMap: Map<string, Dimension>): ViewOptions { + + let viewOpt: ViewOptions = { + metrics: [], + dimensions: [], + materialized: true, + childViews: [], + }; + + for (let i = 0; i < opts.metrics.length; ++i) { + if (metMap.has(opts.metrics[i])) { + viewOpt.metrics.push(metMap.get(opts.metrics[i])); + } + + else { + throw new Error("[Parsing error] Non exist metric set to view " + opts.alias); + } + } + + for (let i = 0; i < opts.dimensions.length; ++i) { + if (dimMap.has(opts.dimensions[i])) { + viewOpt.dimensions.push(dimMap.get(opts.dimensions[i])); + } + + else { + throw new Error("[Parsing error] Non exist dimension set to view " + opts.alias); + } + } + return viewOpt; + } +} diff --git a/test/postgres/fixture.ts b/test/postgres/fixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..111bda10cd75c08400606aed11f4a63a21e281aa --- /dev/null +++ b/test/postgres/fixture.ts @@ -0,0 +1,151 @@ +/* + * 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 { Client, PoolConfig } from "pg"; +import { View, LoadView } from "../../src/core/view"; +import { each, series } from "async"; +import * as fs from "fs"; + +interface TransSet { + init: string; + data: string[]; +} + +export interface Schema { + alias?: string; + query?: string; + data?: string; + fields: any[]; +} + +export class Fixture { + private database: string; + private config: PoolConfig; + + constructor(config: PoolConfig) { + this.config = config; + this.database = config.database; + } + + public load(schemas: LoadView[], create: boolean, cb: (err: Error) => void) { + let client = new Client(this.config); + + /* + Loading data has 2 steps: + 1 - Create a table or truncate a existing one. + Create or truncate dependes from a parameter in the configure + file, this parameter reaches this function as the create + parameter. + 2 - Insert data. + */ + let init: string[] = []; + let data: string[] = []; + for (let i = 0; i < schemas.length; ++i) { + let table: TransSet = this.createTransSet(schemas[i].view, schemas[i].data, create); + init.push(table.init); + data = data.concat(table.data); + } + + client.connect((error) => { + if (error) { + cb(error); + return; + } + + /* + Tables must be creates before data could be inserted, so the + queries that create and insert are splited in 2 arrays. + To garantee that tables exists before start insert data, series + is used. Inside series each query is executed, using each; + */ + series([(callback) => { + each(init, (query, cback) => { + return client.query(query, [], (err: Error) => cback(err)); + }, (errQuery: Error) => callback(errQuery)); + }, (callback) => { + each(data, (query, cback) => { + return client.query(query, [], (err: Error) => cback(err)); + }, (errQuery: Error) => callback(errQuery)); + }], (errQuery: Error) => { + if (errQuery) { + client.end(); + cb(errQuery); + } + client.end((err) => { + cb(err); + }); + }); + + }); + } + + private typeConvertion(t: string) { + switch (t) { + case "integer": + return "INTEGER"; + case "date": + return "DATE"; + case "string": + return "TEXT"; + case "boolean": + return "BOOLEAN"; + default: + return ""; + } + } + + private createTransSet(view: View, filePath: string, create: boolean) { + let props = []; + + for (let i = 0; i < view.metrics.length; ++i) { + let met = view.metrics[i]; + props.push("\"" + met.name + "\" " + this.typeConvertion(met.dataType)); + } + + for (let i = 0; i < view.dimensions.length; ++i) { + let dim = view.dimensions[i]; + props.push("\"" + dim.name + "\" " + this.typeConvertion(dim.dataType)); + } + + let name = "view_" + view.id; + let transaction: TransSet = {init: "", data: []}; + if (create) { + transaction.init = "CREATE TABLE " + name + "(" + props.join(", ") + ")"; + } + else { + transaction.init = "TRUNCATE TABLE " + name; + } + + transaction.data = []; + let rows = JSON.parse(fs.readFileSync(filePath, {encoding : "utf8"})); + for (let i = 0; i < rows.length; ++i) { + let values = []; + let keys = []; + for (let key in rows[i]) { + keys.push("\"" + key + "\""); + values.push("'" + rows[i][key] + "'"); + } + transaction.data.push("INSERT INTO " + name + + "(" + keys.join(", ") + ") " + + "VALUES (" + values.join(", ") + ")"); + } + return transaction; + } +} diff --git a/test/postgres/fixtures/view1.json b/test/postgres/fixtures/view1.json new file mode 100644 index 0000000000000000000000000000000000000000..ee901bd78333d518094fcdb3fa1b2f8b5e6eec70 --- /dev/null +++ b/test/postgres/fixtures/view1.json @@ -0,0 +1,7 @@ +[ +{"dim:1":"2017-01-01","dim:2":"2017-01-01","met:1":"1","met:2":"1","met:3":"1"}, +{"dim:1":"2017-01-02","dim:2":"2017-01-02","met:1":"2","met:2":"2","met:3":"2"}, +{"dim:1":"2017-01-03","dim:2":"2017-01-03","met:1":"3","met:2":"3","met:3":"3"}, +{"dim:1":"2017-01-04","dim:2":"2017-01-04","met:1":"4","met:2":"4","met:3":"4"}, +{"dim:1":"2017-01-05","dim:2":"2017-01-05","met:1":"5","met:2":"5","met:3":"5"} +] diff --git a/test/postgres/fixtures/view2.json b/test/postgres/fixtures/view2.json new file mode 100644 index 0000000000000000000000000000000000000000..b49410e0a3c58c0a19e4931db55c7d4142a9da63 --- /dev/null +++ b/test/postgres/fixtures/view2.json @@ -0,0 +1,7 @@ +[ +{"dim:1":"2017-01-01","dim:2":"2017-01-01","met:1":"1","met:3":"1","met:5":"1"}, +{"dim:1":"2017-01-02","dim:2":"2017-01-02","met:1":"2","met:3":"2","met:5":"2"}, +{"dim:1":"2017-01-03","dim:2":"2017-01-03","met:1":"3","met:3":"3","met:5":"3"}, +{"dim:1":"2017-01-04","dim:2":"2017-01-04","met:1":"4","met:3":"4","met:5":"4"}, +{"dim:1":"2017-01-05","dim:2":"2017-01-05","met:1":"5","met:3":"5","met:5":"5"} +] diff --git a/test/postgres/fixtures/view3.json b/test/postgres/fixtures/view3.json new file mode 100644 index 0000000000000000000000000000000000000000..a45a44119ba3b1011327859d3b6f2cf9a7eda2ed --- /dev/null +++ b/test/postgres/fixtures/view3.json @@ -0,0 +1,7 @@ +[ +{"dim:4":"dim:4:1","dim:5":"dim:5:1","met:3":"1","met:4":"1","met:7":"1"}, +{"dim:4":"dim:4:2","dim:5":"dim:5:2","met:3":"2","met:4":"2","met:7":"2"}, +{"dim:4":"dim:4:3","dim:5":"dim:5:3","met:3":"3","met:4":"3","met:7":"3"}, +{"dim:4":"dim:4:4","dim:5":"dim:5:4","met:3":"4","met:4":"4","met:7":"4"}, +{"dim:4":"dim:4:5","dim:5":"dim:5:5","met:3":"5","met:4":"5","met:7":"5"} +] diff --git a/test/postgres/fixtures/view4.json b/test/postgres/fixtures/view4.json new file mode 100644 index 0000000000000000000000000000000000000000..7dbaad02ce12c5d452cb2b1071d1407082588d15 --- /dev/null +++ b/test/postgres/fixtures/view4.json @@ -0,0 +1,7 @@ +[ +{"dim:3":"1","dim:4":"dim:4:1","dim:5":"dim:5:1","dim:6":"t","met:6":"1","met:7":"1"}, +{"dim:3":"2","dim:4":"dim:4:2","dim:5":"dim:5:2","dim:6":"t","met:6":"2","met:7":"2"}, +{"dim:3":"3","dim:4":"dim:4:3","dim:5":"dim:5:3","dim:6":"f","met:6":"3","met:7":"3"}, +{"dim:3":"4","dim:4":"dim:4:4","dim:5":"dim:5:4","dim:6":"f","met:6":"4","met:7":"4"}, +{"dim:3":"5","dim:4":"dim:4:5","dim:5":"dim:5:5","dim:6":"f","met:6":"5","met:7":"5"} +] diff --git a/test/postgres/fixtures/view5.json b/test/postgres/fixtures/view5.json new file mode 100644 index 0000000000000000000000000000000000000000..3f76ce6e4a3bffffe0e68e79dc45addcc71f51ba --- /dev/null +++ b/test/postgres/fixtures/view5.json @@ -0,0 +1,7 @@ +[ +{"dim:1":"2017-01-01","dim:2":"2017-01-01","dim:7":"1","met:2":"1","met:3":"1","met:8":"1"}, +{"dim:1":"2017-01-02","dim:2":"2017-01-02","dim:7":"2","met:2":"2","met:3":"2","met:8":"2"}, +{"dim:1":"2017-01-03","dim:2":"2017-01-03","dim:7":"3","met:2":"3","met:3":"3","met:8":"3"}, +{"dim:1":"2017-01-04","dim:2":"2017-01-04","dim:7":"4","met:2":"4","met:3":"4","met:8":"4"}, +{"dim:1":"2017-01-05","dim:2":"2017-01-05","dim:7":"5","met:2":"5","met:3":"5","met:8":"5"} +] diff --git a/test/postgres/fixtures/view6.json b/test/postgres/fixtures/view6.json new file mode 100644 index 0000000000000000000000000000000000000000..a810edb99a0efde8151a9c784fba3043c527aadf --- /dev/null +++ b/test/postgres/fixtures/view6.json @@ -0,0 +1,7 @@ +[ +{"dim:1":"2017-01-01","dim:2":"2017-01-01","met:1":"1","met:4":"1"}, +{"dim:1":"2017-01-02","dim:2":"2017-01-02","met:1":"2","met:4":"2"}, +{"dim:1":"2017-01-03","dim:2":"2017-01-03","met:1":"3","met:4":"3"}, +{"dim:1":"2017-01-04","dim:2":"2017-01-04","met:1":"4","met:4":"4"}, +{"dim:1":"2017-01-05","dim:2":"2017-01-05","met:1":"5","met:4":"5"} +] diff --git a/test/postgres/fixtures/view7.json b/test/postgres/fixtures/view7.json new file mode 100644 index 0000000000000000000000000000000000000000..bc4f17dee3fc46baadab5372f8b261c5eedebf1d --- /dev/null +++ b/test/postgres/fixtures/view7.json @@ -0,0 +1,7 @@ +[ +{"dim:8":"1","dim:9":"2017-01-01","dim:10":"dim:5:1","met:8":"1"}, +{"dim:8":"2","dim:9":"2017-01-02","dim:10":"dim:5:2","met:8":"2"}, +{"dim:8":"3","dim:9":"2017-01-03","dim:10":"dim:5:3","met:8":"3"}, +{"dim:8":"4","dim:9":"2017-01-04","dim:10":"dim:5:4","met:8":"4"}, +{"dim:8":"5","dim:9":"2017-01-05","dim:10":"dim:5:5","met:8":"5"} +] diff --git a/test/postgres/fixtures/view8.json b/test/postgres/fixtures/view8.json new file mode 100644 index 0000000000000000000000000000000000000000..db33365b4780efabe114f1a85b65139978852ff9 --- /dev/null +++ b/test/postgres/fixtures/view8.json @@ -0,0 +1,7 @@ +[ +{"dim:8":"1","dim:9":"2017-01-01","dim:10":"dim:5:1","met:9":"1"}, +{"dim:8":"2","dim:9":"2017-01-02","dim:10":"dim:5:2","met:9":"2"}, +{"dim:8":"3","dim:9":"2017-01-03","dim:10":"dim:5:3","met:9":"3"}, +{"dim:8":"4","dim:9":"2017-01-04","dim:10":"dim:5:4","met:9":"4"}, +{"dim:8":"5","dim:9":"2017-01-05","dim:10":"dim:5:5","met:9":"5"} +] diff --git a/test/postgres/fixtures/view9.json b/test/postgres/fixtures/view9.json new file mode 100644 index 0000000000000000000000000000000000000000..c6ef8937a25975af513a957f8e7f5ea9d32e1266 --- /dev/null +++ b/test/postgres/fixtures/view9.json @@ -0,0 +1,7 @@ +[ +{"dim:8":"1","dim:9":"2017-01-01","dim:10":"dim:5:1","met:10":"1"}, +{"dim:8":"2","dim:9":"2017-01-02","dim:10":"dim:5:2","met:10":"2"}, +{"dim:8":"3","dim:9":"2017-01-03","dim:10":"dim:5:3","met:10":"3"}, +{"dim:8":"4","dim:9":"2017-01-04","dim:10":"dim:5:4","met:10":"4"}, +{"dim:8":"5","dim:9":"2017-01-05","dim:10":"dim:5:5","met:10":"5"} +] diff --git a/test/scenario.ts b/test/scenario.ts new file mode 100644 index 0000000000000000000000000000000000000000..24185847b66e7aeddd6b615142a2b9e6bee1baa2 --- /dev/null +++ b/test/scenario.ts @@ -0,0 +1,358 @@ +/* + * 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 { Metric } from "../src/core/metric"; +import { Dimension } from "../src/core/dimension"; +import { View } from "../src/core/view"; +import { AggregationType, RelationType } from "../src/common/types"; + +interface EngineScenario { + metrics: Metric[]; + dimensions: Dimension[]; + subDimensions: Dimension[]; + views: View[]; +} + +interface AdapterScenario { + materializedView: View; + noSelectionView: View; + withSelectionView: View; + subDimensionView: View; + join4View: View; + dateView: View; +} + +const mets = [ + new Metric({ name: "met:1", aggregation: AggregationType.SUM, dataType: "integer" }), + new Metric({ name: "met:2", aggregation: AggregationType.AVG, dataType: "integer" }), + new Metric({ name: "met:3", aggregation: AggregationType.AVG, dataType: "integer" }), + new Metric({ name: "met:4", aggregation: AggregationType.SUM, dataType: "integer" }), + new Metric({ name: "met:5", aggregation: AggregationType.SUM, dataType: "integer" }), + new Metric({ name: "met:6", aggregation: AggregationType.AVG, dataType: "integer" }), + new Metric({ name: "met:7", aggregation: AggregationType.COUNT, dataType: "integer" }), + new Metric({ name: "met:8", aggregation: AggregationType.COUNT, dataType: "integer" }), + new Metric({ name: "met:9", aggregation: AggregationType.SUM, dataType: "integer" }), + new Metric({ name: "met:10", aggregation: AggregationType.COUNT, dataType: "integer" }), +]; + +const dims = [ + new Dimension({ name: "dim:1", dataType: "date" }), + new Dimension({ name: "dim:2", dataType: "date" }), + new Dimension({ name: "dim:3", dataType: "integer" }), + new Dimension({ name: "dim:4", dataType: "string" }), + new Dimension({ name: "dim:5", dataType: "string" }), + new Dimension({ name: "dim:6", dataType: "boolean" }), + new Dimension({ name: "dim:7", dataType: "integer" }), + new Dimension({ name: "dim:8", dataType: "integer" }), + new Dimension({ name: "dim:9", dataType: "date" }), + new Dimension({ name: "dim:10", dataType: "string" }), +]; + +const wrongMet = new Metric({ + name: "met:11", + aggregation: AggregationType.COUNT, + dataType: "integer" +}); +const wrongDim = new Dimension({ name: "dim:11", dataType: "integer" }); + +const subdimAux = new Dimension({ + name: "sub:1", + dataType: "integer", + parent: dims[0], + relation: RelationType.DAY +}); + +const subdims = [ + subdimAux, + new Dimension({ + name: "sub:2", + dataType: "integer", + parent: dims[8], + relation: RelationType.DAY + }), + new Dimension({ + name: "sub:3", + dataType: "integer", + parent: subdimAux, + relation: RelationType.DAY + }), + new Dimension({ + name: "sub:4", + dataType: "integer", + parent: null, + relation: RelationType.DAY + }), + new Dimension({ + name: "sub:5", + dataType: "integer", + parent: dims[1], + relation: RelationType.DAY + }) +]; + +const dateSubDim = [ + new Dimension ({ + name: "dim:2:month", + dataType: "integer", + parent: dims[1], + relation: RelationType.MONTH + }), + new Dimension ({ + name: "dim:2:day", + dataType: "integer", + parent: dims[1], + relation: RelationType.DAY + }), + new Dimension ({ + name: "dim:2:year", + dataType: "integer", + parent: dims[1], + relation: RelationType.YEAR + }), + new Dimension ({ + name: "dim:2:dow", + dataType: "integer", + parent: dims[1], + relation: RelationType.DAYOFWEEK + }), +]; + +const views = [ + new View({ + metrics: [mets[0], mets[1], mets[2]], + dimensions: [dims[0], dims[1]], + materialized: true + }), + new View({ + metrics: [mets[0], mets[2], mets[4]], + dimensions: [dims[0], dims[1]], + materialized: true + }), + new View({ + metrics: [mets[2], mets[3], mets[6]], + dimensions: [dims[3], dims[4]], + materialized: true + }), + new View({ + metrics: [mets[5], mets[6]], + dimensions: [dims[2], dims[3], dims[4], dims[5]], + materialized: true + }), + new View({ + metrics: [mets[7], mets[1], mets[2]], + dimensions: [dims[0], dims[1], dims[6]], + materialized: true + }), + new View({ + metrics: [mets[0], mets[3]], + dimensions: [dims[0], dims[1]], + materialized: true + }), + new View({ + metrics: [mets[7]], + dimensions: [dims[7], dims[8], dims[9]], + materialized: true + }), + new View({ + metrics: [mets[8]], + dimensions: [dims[7], dims[8], dims[9]], + materialized: true + }), + new View({ + metrics: [mets[9]], + dimensions: [dims[7], dims[8], dims[9]], + materialized: true + }) +]; + +const engAuxView = [ + new View({ + metrics: [mets[0], mets[1], mets[2], mets[3], mets[4]], + dimensions: [dims[0], dims[1]], + materialized: false, + childViews: [ + { + view: views[0], + metrics: [mets[0], mets[1], mets[2]], + dimensions: [dims[0], dims[1]] + }, + { + view: views[5], + metrics: [mets[3]], + dimensions: [] + }, + { + view: views[1], + metrics: [mets[4]], + dimensions: [] + } + ] + }), + + new View({ + metrics: [mets[7], mets[8], mets[9]], + dimensions: [dims[7], dims[8], dims[9]], + materialized: false, + childViews: [ + { + view: views[6], + metrics: [mets[7]], + dimensions: [dims[7], dims[8], dims[9]] + }, + { + view: views[7], + metrics: [mets[8]], + dimensions: [] + }, + { + view: views[8], + metrics: [mets[9]], + dimensions: [] + } + ] + }), + + new View({ + metrics: [mets[0]], + dimensions: [subdims[0], subdims[1]], + materialized: false, + childViews: [ + { + view: views[0], + metrics: [mets[0]], + dimensions: [dims[0]] + }, + { + view: views[9], + metrics: [], + dimensions: [dims[8]] + } + ] + }) +]; + +const dateView = new View({ + metrics: [], + dimensions: dateSubDim, + materialized: false, + childViews: [{ + view: views[0], + metrics: [], + dimensions: [dims[1]] + }] +}); + +const subDimView = new View({ + metrics: [mets[0]], + dimensions: [subdims[0], subdims[1]], + materialized: false, + childViews: [ + { + view: views[0], + metrics: [mets[0]], + dimensions: [dims[0]] + }, + { + view: views[8], + metrics: [], + dimensions: [dims[8]] + } + ] +}); + +const join4View = new View({ + metrics: mets, + dimensions: dims, + materialized: false, + childViews: [ + { + view: engAuxView[0], + metrics: [mets[0], mets[1], mets[2], mets[3], mets[4]], + dimensions: [dims[0], dims[1]] + }, + { + view: engAuxView[1], + metrics: [mets[7], mets[8], mets[9]], + dimensions: [dims[7], dims[8], dims[9]] + }, + { + view: views[3], + metrics: [mets[5], mets[6]], + dimensions: [dims[2], dims[3], dims[4], dims[5]] + }, + { + view: views[4], + metrics: [], + dimensions: [dims[6]] + } + ] +}); + +const noSelView = new View({ + metrics: [mets[0], mets[3]], + dimensions: [], + materialized: false, + childViews: [ + { + view: views[0], + metrics: [mets[0]], + dimensions: [] + }, + { + view: views[2], + metrics: [mets[3]], + dimensions: [] + } + ] +}); + +const withSelView = new View({ + metrics: [mets[0], mets[1], mets[4]], + dimensions: [dims[0], dims[1]], + materialized: false, + childViews: [ + { + view: views[0], + metrics: [mets[0], mets[1]], + dimensions: [dims[0], dims[1]] + }, + { + view: views[1], + metrics: [mets[4]], + dimensions: [] + } + ] +}); + +export const engineScenario: EngineScenario = { + metrics: mets.concat([wrongMet]), + dimensions: dims.concat([wrongDim]), + subDimensions: subdims, + views: views.concat(engAuxView) +}; + +export const adapterScenario: AdapterScenario = { + materializedView: views[0], + noSelectionView: noSelView, + withSelectionView: withSelView, + subDimensionView: subDimView, + join4View: join4View, + dateView: dateView +};