Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • c3sl/blendb
  • pdts20/blendb
2 results
Select Git revision
Show changes
Showing
with 1772 additions and 170 deletions
/*
* 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 { Middleware } from "../types";
import { Adapter } from "../../core/adapter";
import { PostgresAdapter } from "../../adapter/postgres";
import { MonetAdapter, MonetConfig } from "../../adapter/monet";
import { PoolConfig } from "pg";
import { Connection } from "../../util/configParser";
/**
* Creates a PostgreSQL adapter and middleware that
* inserts the adapter into the request objects.
* @param config - Parameters required to connect in database.
*/
export function PostgresMw(config: Connection): Middleware {
let parsedConfig: PoolConfig = {
user: config.user,
database: config.database,
password: config.password,
host: config.host,
port: config.port,
max: 10,
idleTimeoutMillis: 3000
};
let adapter: Adapter = new PostgresAdapter(parsedConfig);
return function postgresMiddleware(req, res, next) {
req.adapter = adapter;
next();
};
}
/**
* Creates a MonetDB adapter and middleware that
* inserts the adapter into the request objects.
* @param config - Parameters required to connect in database.
*/
export function MonetMw(config: Connection): Middleware {
let parsedConfig: MonetConfig = {
user: config.user,
dbname: config.database,
password: config.password,
host: config.host,
port: config.port,
};
let adapter: Adapter = new MonetAdapter(parsedConfig);
return function monetMiddleware(req, res, next) {
req.adapter = adapter;
next();
};
}
/*
* Copyright (C) 2015-2019 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 * as json2csv from "json-2-csv";
import { Middleware } from "../types";
/**
* Creates a csv parser and middleaew that
* inserts the parser into the request objects.
*/
export function CsvMw(): Middleware {
return function csvMiddleware(req, res, next) {
req.csvParser = function parseCsv(json: any, format: string, cb) {
const separator = format.substring(0, 1);
let sep = ",";
if (separator === "s") {
sep = ";";
}
else if (separator === "t"){
sep = "\t";
}
json2csv.json2csv(json, cb, {
delimiter: {
field: sep
, wrap: "\""
, eol: "\n"
}
, prependHeader: true
});
};
next();
};
}
/*
* 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 { Engine } from "../../core/engine";
import { ParsedConfig } from "../../util/configParser";
import { Middleware } from "../types";
/**
* Creates a engine and middleware that
* inserts the engine into the request objects.
* @param config - Parsed database schema.
*/
export function EngineMw (config: ParsedConfig): Middleware {
let engine: Engine = new Engine(config);
return function engineMiddleware(req, res, next) {
req.engine = engine;
next();
};
}
/*
* 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 * as request from "supertest";
import { expect } from "chai";
import * as server from "../../main";
describe("API error middleware", () => {
it("should respond 404 when a route does not exists", (done) => {
request(server)
.get("/v1/inexistent")
.expect(404)
.expect((res: any) => {
const error = "Cannot GET /v1/inexistent";
expect(res.body).to.be.an("object");
expect(res.body).to.have.property("error");
expect(res.body.error).to.be.eql(error);
})
.end(done);
});
it("should respond 400 for missing required parameters", (done) => {
request(server)
.get("/v1/data")
.query({metrics: "random"})
.expect(400)
.expect((res: any) => {
const error = "Bad request: Some problems with this request were found";
expect(res.body).to.be.an("object");
expect(res.body).to.have.property("error");
expect(res.body).to.have.property("requestErrors");
expect(res.body.error).to.be.eql(error);
expect(res.body.requestErrors).to.have.length(1);
})
.end(done);
});
it("should respond 400 when you connect, and do not send data", (done) => {
request(server)
.post("/v1/collect/Seller")
.expect(400)
.expect((res: any) => {
expect(res.body).to.be.an("object");
expect(res.body).to.have.property("error");
})
.end(done);
});
});
/* /*
* Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre * Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana * Departamento de Informatica - Universidade Federal do Parana
* *
* This file is part of blendb. * This file is part of blendb.
...@@ -18,70 +18,54 @@ ...@@ -18,70 +18,54 @@
* along with blendb. If not, see <http://www.gnu.org/licenses/>. * along with blendb. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Hash } from "../util/hash"; import * as express from "express";
import { Source } from "./source"; /**
import { Aggregate } from "./aggregate"; * Creates a middleware to handle errors proper to each API version.
* @param config - API version.
export interface ITransformerOptions { */
source: Source; export function ErrorMw(config: string): express.ErrorRequestHandler {
destination: Aggregate; const handlers: { [key: string]: express.ErrorRequestHandler } = {
functions: { "v1": function(err, req, res, next) {
map: (doc: any, emit: Function) => void; if (err.ramlNotFound) {
reduce: (dimensions: any, metrics: any) => any; const splited = err.toString().split(" ");
}; const method = splited[1];
const route = "/v1" + splited[2];
res.status(404).json({ error: "Cannot " + method + " " + route });
} }
export class Transformer { else if (err.ramlValidation) {
public name: string; res.status(400).json({
public source: Source; error: "Bad request: Some problems with this request were found"
public destination: Aggregate; , requestErrors: err.requestErrors
private functions: any; });
}
constructor(name: string, options: ITransformerOptions) { else if (err.ramlAuthorization) {
this.name = name; res.status(401).json({
this.source = options.source; error: "Unauthorized: Some problems with your authentication were found, try to authenticate again"
this.destination = options.destination; , requestErrors: err.authorizationErrors
this.functions = { });
map: options.functions.map,
reduce: options.functions.reduce
};
} }
public apply() { else {
let temp = new Map(); res.status(400).json({error: err.toString()});
}
this.destination.truncate(); return;
},
this.source.forEach((doc: any) => { "default": function(err, req, res, next) {
let emit = (dimensions: any, metrics: any) => { next(err);
let key = Hash.sha1(dimensions); return;
let current = temp.get(key) || { dimensions, metrics: [] }; }
temp.set(key, {
dimensions,
metrics: current.metrics.concat([metrics])
});
}; };
this.functions.map(doc, emit); if (handlers[config]) {
}); return handlers[config];
temp.forEach((value, key) => {
let dimensions = value.dimensions;
let metrics = value.metrics;
if (metrics.length > 1) {
this.destination.push({
dimensions: dimensions,
metrics: this.functions.reduce(dimensions, metrics)
});
} }
else { else {
this.destination.push({ return handlers.default;
dimensions: dimensions,
metrics: metrics
});
}
});
}
} }
};
/* /*
* Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana * Departamento de Informatica - Universidade Federal do Parana
* *
* This file is part of blend. * This file is part of blendb.
* *
* blend is free software: you can redistribute it and/or modify * blendb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* blend is distributed in the hope that it will be useful, * blendb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with blend. If not, see <http://www.gnu.org/licenses/>. * along with blendb. If not, see <http://www.gnu.org/licenses/>.
*/ */
export enum AggregationType { import { Log } from "../../util/log";
SUM, import { Middleware } from "../types";
AVG,
COUNT, /**
NONE * Creates a log and middleware that
* inserts the log into the request objects.
*/
export function LogMw (): Middleware {
let log: Log = new Log();
return function logMiddleware(req, res, next) {
req.log = log;
next();
}; };
}
...@@ -18,13 +18,21 @@ ...@@ -18,13 +18,21 @@
* along with blendb. If not, see <http://www.gnu.org/licenses/>. * along with blendb. If not, see <http://www.gnu.org/licenses/>.
*/ */
/** @hidden */
const osprey = require("osprey"); const osprey = require("osprey");
// import controllers // import controllers
import { DataCtrl } from "./controllers/data"; import { DataCtrl } from "./controllers/data";
import { CollectCtrl } from "./controllers/collect"; import { CollectCtrl } from "./controllers/collect";
import { EngineCtrl } from "./controllers/engine";
/** @hidden */
export const router = osprey.Router(); export const router = osprey.Router();
router.get("/metrics", EngineCtrl.metrics);
router.get("/sources", EngineCtrl.sources);
router.get("/dimensions", EngineCtrl.dimensions);
router.get("/enumtypes", EngineCtrl.enumTypes);
router.get("/tags", EngineCtrl.tags);
router.get("/data", DataCtrl.read); router.get("/data", DataCtrl.read);
router.post("/collect/{class}", CollectCtrl.write); router.post("/collect/{class}", CollectCtrl.write);
/*
* 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 * as express from "express";
import { Engine } from "../core/engine";
import { Adapter} from "../core/adapter";
import { Log } from "../core/log";
/**
* Extension of Express requests that suports the addition
* of an engine and an adapter.
* This extension is required because some Middlewares
* add some objetcs, in this case metrics and dimensions,
* to the Request object. To typescript compiler do not
* return a error the extension must be made.
*/
export default interface Request extends express.Request {
/** A engine object. Represents the database in BlenDB perspective. */
engine: Engine;
/** A adapter object. Used to communicate with the database in use. */
adapter: Adapter;
/** A csvParser function. Used to parse json object into csv file. */
csvParser: (json: any, format: string, cb: (err: Error, csv?: string));
/** A log object. Used store logs into file. */
log: Log;
}
/**
* Extension Middleware function of ExpressJS Module.
* Uses the custom Request object and is used to define
* the middlewares of BlenDB API.
*/
export interface Middleware {
(req: Request, res: express.Response, next: express.NextFunction): void;
}
/*
* Copyright (C) 2018 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blend.
*
* blend is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* blend is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with blend. If not, see <http://www.gnu.org/licenses/>.
*/
import { View } from "../core/view";
/**
* Operation codes to each allowed operation over a view.
*/
export enum Opcode {
/**
* Push operation.
* This operation means that the view is materialized.
* This also means that there are no need to create it
* using other views.
* And also means that this view is a valid data location.
*/
PUSH,
/**
* Join operation.
* The view will be created using a set of others view.
* This operation is equivalent a INNER JOIN of SQL
* or as close as possible of JOIN.
*/
JOIN,
/**
* Reduce operation.
* The reduce operation removes irrelevant metrics and
* dimensions and apply clauses. Represents the Projection
* and Selecion operations of Relational Algebra.
*/
REDUCE
}
/**
* Defines how to construct a View with a operation
* and a set of chidren Views.
*/
export interface Operation {
/** Operation code used. */
opcode: Opcode;
/** Set of views required to perform the operation. */
values: View[];
}
...@@ -18,8 +18,41 @@ ...@@ -18,8 +18,41 @@
* along with blend. If not, see <http://www.gnu.org/licenses/>. * along with blend. If not, see <http://www.gnu.org/licenses/>.
*/ */
export interface Query { import { Metric } from "../core/metric";
public metrics: string[]; import { Dimension } from "../core/dimension";
public dimensions: string[]; import { Clause } from "../core/clause";
/**
* Internal representation of a query in BlenDB perspective.
*/
export interface QueryOpts {
/** Set of metrics of the query. */
metrics: Metric[];
/** Set of dimensions of the query. */
dimensions: Dimension[];
/** Set of clauses of the query. */
clauses?: Clause[];
/** List of metrics and dimensions to sort the query. */
sort?: (Metric | Dimension)[];
}
export class Query {
/** Set of metrics of the query. */
public readonly metrics: Metric[];
/** Set of dimensions of the query. */
public readonly dimensions: Dimension[];
/** Set of clauses of the query. */
public readonly clauses: Clause[];
/** List of metrics and dimensions to sort the query. */
public readonly sort: (Metric | Dimension)[];
/**
* Create Query
* @param opts - Parameters required to create a query.
*/
constructor(opts: QueryOpts) {
this.metrics = opts.metrics.map((i) => i);
this.dimensions = opts.dimensions.map((i) => i);
this.clauses = opts.clauses ? opts.clauses.map((i) => i) : [];
this.sort = opts.sort ? opts.sort.map((i) => i) : [];
}
} }
/*
* Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blendb.
*
* 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.
*
* 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/>.
*/
/**
* Parameters used to create a Tag object.
* Parameters used to define tag object in the configuration file.
* Also the string description of a tag.
*/
export interface TagOptions {
/** Tag name. */
name: string;
/** Breif description of what this tag represents. */
description?: string;
}
/**
* A Tag can be attached to other elements (such as metrics and dimensions)
* to create groups of similar elements. A tag can also be used to filter
* these elements, search for itens that only contain the given tags.
*/
export class Tag {
/** Tag name. */
public readonly name: string;
/** Breif description of what this tag represents. */
public readonly description: string;
/**
* Creates a tag.
* @param options - Parameters required to create a tag.
*/
constructor(options: TagOptions) {
this.name = options.name;
this.description = (options.description) ? options.description : "";
}
/**
* Creates a object with the same options used to create this
* tag as strings. Used to inform the API users.
*/
public strOptions(): TagOptions {
return {
name: this.name
, description: this.description
};
}
}
/*
* Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blend.
*
* blend is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* blend is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with blend. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Available aggregation function to metrics.
*/
export enum AggregationType {
/** Used as error code. So suitable aggregation found. */
NONE,
/** Sum aggregation function. Sum of all registers. */
SUM,
/** Average aggregation function. Average of all registers. */
AVG,
/** Count agggreation function. Count the number of registers. */
COUNT,
/** Maximum aggregation function. The biggest value of all registers. */
MAX,
/** Minimum aggregation function. The smallest value of all registers. */
MIN
}
/**
* Available relationships between sub dimensions and its parents.
*/
export enum RelationType {
/** Used when there are no relation. The dimension is not a sub dimension. */
NONE,
/**
* Day relation.
* The parent is a timestamp and the dimension is only the day of
* the timestamp.
*/
DAY,
/**
* Month relation.
* The parent is a timestamp and the dimension is only the month of
* the timestamp.
*/
MONTH,
/**
* Year relation.
* The parent is a timestamp and the dimension is only the year of
* the timestamp.
*/
YEAR,
}
/**
* Available data types for metrics, dimensions and source members.
* In other words types that BlenDB can handle.
*/
export enum DataType {
/** Used as error code when no suitable type is found. */
NONE,
/** Interger type. */
INTEGER,
/** Floating number type. */
FLOAT,
/** String type. */
STRING,
/** Timestamp type. ISO format */
DATE,
/** True/False type. */
BOOLEAN,
/**
* Enumerable type.
* Custom type, that only accepts a set of previeous defined values.
* Custom enumeration defined in configuration file.
*/
ENUMTYPE
}
...@@ -19,8 +19,30 @@ ...@@ -19,8 +19,30 @@
*/ */
import { View } from "./view"; import { View } from "./view";
import { Source } from "./source";
/**
* Absraction of the SGBD from the BlenDB perspective. Used to
* perform all the operations into the database that the Blendb
* requires. This operations include read and write data.
*/
export abstract class Adapter { export abstract class Adapter {
public abstract getDataFromView(view: View): string; /**
public abstract materializeView(view: View): string; * Asynchronously reads all data from given view.
* In other words perform a SELECT query.
* @param view - "Location" from all data should be read.
* @param cb - Callback function which contains the data read.
* @param cb.error - Error information when the method fails.
* @param cb.result - Data got from view.
*/
public abstract getDataFromView(view: View, cb: (err: Error, result: any[]) => void): void;
/**
* Asynchronously insert one register into a given Source.
* @param source - Insertion "location".
* @param data - Data to be inserted.
* @param cb - Callback function which contains the query result.
* @param cb.error - Error information when the method fails.
* @param cb.result - Query result.
*/
public abstract insertIntoSource(source: Source, data: any[], cb: (err: Error, result: any[]) => void): void;
} }
/* /*
* Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre * Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana * Departamento de Informatica - Universidade Federal do Parana
* *
* This file is part of blend. * This file is part of blend.
...@@ -18,63 +18,59 @@ ...@@ -18,63 +18,59 @@
* along with blend. If not, see <http://www.gnu.org/licenses/>. * along with blend. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Filter } from "./filter";
import { Hash } from "../util/hash"; import { Hash } from "../util/hash";
import { Dimension } from "./dimension";
import { Metric } from "./metric";
import { Source } from "./source"; /** Parameters used to create a Clause object. */
import { Transformer, ITransformerOptions } from "./transformer"; export interface ClauseOptions {
import { Aggregate } from "./aggregate"; /** Set of filters that form the clause. */
filters: Filter[];
export class Server {
private sources: Map<string, Source>;
private transformers: Map<string, Transformer>;
private aggregates: Map<string, Aggregate>;
constructor() {
this.sources = new Map();
this.transformers = new Map();
this.aggregates = new Map();
}
public source(name: string, options?: any) {
if (this.sources.has(name)) {
return this.sources.get(name);
}
else {
const source = new Source(name, options);
this.sources.set(name, source);
return source;
}
} }
public aggregate(metrics: string[], dimensions: string[], options?: any) { /** Set of restrictions applied to a query united by the operator OR. */
const id = Hash.sha1(metrics.sort(), dimensions.sort()); export class Clause {
/** Hash of components that unique identify the clause. */
public readonly id: string;
/** Set of filters the form the clause. */
public readonly filters: Filter[];
/** Set of attributes affected by this clause. */
public readonly targets: (Metric|Dimension)[];
if (this.aggregates.has(id)) { /**
return this.aggregates.get(id); * Create a clause object.
} * @param options - Parameters required to create a clause object.
else { */
const aggregate = new Aggregate(metrics, dimensions, options); constructor (options: ClauseOptions) {
this.aggregates.set(id, aggregate); this.filters = options.filters.map((i) => i);
return aggregate; const t = this.filters.map((i) => i.target).sort((a, b) => {
} return (a.name < b.name) ? -1 : 1;
});
if (t.length > 0) {
this.targets = [t[0]];
for (let i = 1; i < t.length; ++i) {
if (t[i - 1] !== t[i]) {
this.targets.push(t[i]);
} }
public transformer(name: string, options?: ITransformerOptions) {
if (typeof options !== "undefined") {
if (this.transformers.has(name)) {
throw new Error("A transformer named '" + name + "' already exists");
} }
const transformer = new Transformer(name, options);
this.transformers.set(name, transformer);
return transformer;
} }
else { else {
if (!this.transformers.has(name)) { this.targets = [];
throw new Error("A transformer named '" + name + "' does not exist");
} }
const filtersIds = this.filters.map((item) => item.id);
return this.transformers.get(name); this.id = Hash.sha1(filtersIds.sort());
} }
/**
* Checks if the targets of this clause is a sub set of
* the given coverage set.
* In other words, this clause could be applied to a view
* which has the coverage set as set of metrics and dimensions
* @param coverage - Set of attributes which will be verified if
* contain all targets in the clause.
*/
public isCovered(coverage: (Metric|Dimension)[]): boolean {
return this.targets.every((i) => coverage.some((j) => i.name === j.name));
} }
} }
/*
* Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blend.
*
* blend is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* blend is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with blend. If not, see <http://www.gnu.org/licenses/>.
*/
import { RelationType, DataType } from "../common/types";
import { EnumHandler } from "../util/enumHandler";
import { Tag } from "../common/tag";
/** Parameters used to create a Dimension object. */
export interface DimensionOptions {
/** Dimension name. */
name: string;
/** Dimension data type. */
dataType: DataType;
/** Parent dimension, in case this dimension is a sub dimenion. */
parent?: Dimension;
/** Relationship with the parent. */
relation?: RelationType;
/** Breif description of what this dimension represents. */
description?: string;
/** Enumerable type name, used if data type is enumerable type. */
enumType?: string;
/** List of tags, a complement to attribute description. */
tags?: Tag[];
}
/**
* Parameters used to define dimension object in the configuration file.
* Also the string description of a dimension.
*/
export interface DimensionStrOptions {
/** Dimension name. */
name: string;
/** Dimension data type. */
dataType: string;
/** Parent dimension, in case this dimension is a sub dimenion. */
parent?: string;
/** Relationship with the parent. */
relation?: string;
/** Breif description of what this dimension represents. */
description?: string;
/** Dimension enum type */
enumType?: string;
/** List of tag names, a complement to attribute description. */
tags?: string[];
}
/**
* One attribute of database that can be read.
* A dimension defines a aspect or characteristic of the data.
* Used in a query as a desired information and is separated from
* the metrics because this two types of attributes behave differently
* in the query. While dimensions are grouped, metrics are aggregated.
*/
export class Dimension {
/** Dimension name. */
public readonly name: string;
/** Dimenion data type. */
public readonly dataType: DataType;
/** Parent dimension, in case this dimension is a sub dimenion. */
public readonly parent: Dimension;
/** Relationship with the parent. */
public readonly relation: RelationType;
/** Breif description of what this dimension represents. */
public readonly description: string;
/** Enumerable type name, used if data type is enumerable type. */
public readonly enumType: string;
/** List of tags, a complement to attribute description. */
public readonly tags: Tag[];
/**
* Creates a dimension.
* @param options - Parameters required to create a dimension.
*/
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;
this.description = (options.description) ? options.description : "";
this.enumType = (options.enumType) ? options.enumType : "";
this.tags = (options.tags) ? options.tags.map((i) => i) : [];
}
/**
* Creates a object with the same options used to create this
* dimension as strings. Used to inform the API users.
*/
public strOptions(): DimensionStrOptions {
let o: DimensionStrOptions = {
name: this.name,
dataType: EnumHandler.stringfyDataType(this.dataType),
description: this.description,
tags: this.tags.map ((i) => i.name)
};
if (this.relation !== RelationType.NONE) {
o.relation = EnumHandler.stringifyRelationType(this.relation);
o.parent = this.parent.name;
}
if (this.dataType === DataType.ENUMTYPE) {
o.enumType = this.enumType;
}
return o;
}
}
...@@ -19,127 +19,314 @@ ...@@ -19,127 +19,314 @@
*/ */
import { expect } from "chai"; import { expect } from "chai";
import { Engine } from "./engine"; import { Engine } from "./engine";
import { FilterOperator } from "./filter";
import { View } from "./view"; import { View } from "./view";
import { ViewOptions } from "./view"; import { engineScenario } from "../../test/scenario";
import { Query } from "../common/query"; import { EnumType } from "./enumType";
import { AggregationType } from "../common/aggregationType"; import { Dimension } from "./dimension";
describe("engine class", () => { describe("engine class", () => {
let query: Query = {
metrics: [],
dimensions: []
};
let views: View[] = []; const engine = new Engine(engineScenario.config);
const subdim = engineScenario.subDimensions;
let viewBuilder: Query[] = [
{ metrics: ["met:1", "met:2", "met:3"], dimensions: ["dim:1", "dim:2"]}, for (let key in subdim){
{ metrics: ["met:1", "met:3", "met:5"], dimensions: ["dim:1", "dim:2"]}, if (subdim[key]){
{ metrics: ["met:3", "met:4", "met:7"], dimensions: ["dim:4", "dim:5"]}, engine.addDimension(subdim[key]);
{ metrics: ["met:6", "met:7"], dimensions: ["dim:3", "dim:4", "dim:5", "dim:6"]}, }
{ metrics: ["met:8", "met:2", "met:3"], dimensions: ["dim:1", "dim:2", "dim:7"]}, }
{ metrics: ["met:1", "met:2", "met:3"], dimensions: ["dim:1", "dim:2"]},
{ metrics: ["met:2", "met:4"], dimensions: ["dim:1", "dim:2"]}, it("should be create a fill that cover all metrics and dimensions", () => {
{ metrics: ["met:8"], dimensions: ["dim:8", "dim:9", "dim:10"]}, let query = engineScenario.queryMetsDims;
{ metrics: ["met:9"], dimensions: ["dim:8", "dim:9", "dim:10"]},
{ metrics: ["met:10"], dimensions: ["dim:8", "dim:9", "dim:10"]}
];
let iterable: [string, AggregationType][] = [
["met:1", AggregationType.SUM],
["met:2", AggregationType.AVG],
["met:3", AggregationType.AVG],
["met:4", AggregationType.SUM],
["met:5", AggregationType.SUM],
["met:6", AggregationType.AVG],
["met:7", AggregationType.COUNT],
["met:8", AggregationType.COUNT],
["met:9", AggregationType.SUM],
["met:10", AggregationType.COUNT],
];
let map: Map<string, AggregationType> = new Map(iterable);
for (let i: number = 0; i < 10; ++ i) {
query.metrics.push("met:" + (i + 1));
query.dimensions.push("dim:" + (i + 1));
let options: ViewOptions = {
metrics: viewBuilder[i].metrics,
dimensions: viewBuilder[i].dimensions,
materialized: true,
aggregationMap: map,
childViews: []
};
views.push(new View (options));
}
views.push(new View({
metrics: ["met:1", "met:2", "met:3", "met:4", "met:5"],
dimensions: ["dim:1", "dim:2"],
materialized: false,
aggregationMap: map,
childViews: [views[0], views[6]]
}));
views.push(new View({
metrics: ["met:8", "met:9", "met:10"],
dimensions: ["dim:8", "dim:9", "dim:10"],
materialized: false,
aggregationMap: map,
childViews: [views[7], views[8], views[9]]
}));
it("should be create a fill that cover the query and has 4 children", () => {
let engine: Engine = new Engine (views);
let optimalView = engine.query(query); let optimalView = engine.query(query);
expect(optimalView).to.be.an("object"); expect(optimalView).to.be.an("object");
expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("metrics");
expect(optimalView).to.have.property("dimensions"); expect(optimalView).to.have.property("dimensions");
expect(optimalView).to.have.property("childViews");
expect(optimalView.metrics).to.be.an("array"); expect(optimalView.metrics).to.be.an("array");
expect(optimalView.dimensions).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array");
expect(optimalView.childViews).to.be.an("array"); expect(optimalView.metrics).to.have.length(16);
expect(optimalView.metrics.length === 10); expect(optimalView.dimensions).to.have.length(19);
expect(optimalView.dimensions.length === 10);
expect(optimalView.childViews.length === 4);
let metAux: number[] = optimalView.metrics.sort().map((item: string) => {
return Number(item.split(":")[1]);
});
let dimAux: number[] = optimalView.dimensions.sort().map((item: string) => {
return Number(item.split(":")[1]);
});
for (let i: number = 1; i <= 10; ++i) {
expect(dimAux[i] === i);
expect(metAux[i] === i);
}
}); });
it("should throw an exception, query with non-existent metric", () => { it("should throw an exception, query with non-existent metric", () => {
let engine: Engine = new Engine (views);
let error: boolean = false; let error: boolean = false;
try { try {
engine.query({metrics: ["met:11"], dimensions: ["dim:1"]}); engine.query(engineScenario.queryNoMets);
} }
catch (e){ catch (e){
error = true; error = true;
expect(e.message).to.be.equal("Engine views cannot cover the query"); expect(e.message).to.be.equal("[Engine Error] Engine views cannot cover the query");
} }
expect(error); expect(error);
}); });
it("should throw an exception, query with non-existent dimension", () => { it("should throw an exception, query with non-existent dimension", () => {
let engine: Engine = new Engine (views);
let error: boolean = false; let error: boolean = false;
try { try {
engine.query({metrics: ["met:1"], dimensions: ["dim:11"]}); engine.query(engineScenario.queryNoDims);
} }
catch (e){ catch (e){
error = true; error = true;
expect(e.message).to.be.equal("Engine views cannot cover the query"); expect(e.message).to.be.equal("[Engine Error] Engine views cannot cover the query");
} }
expect(error); expect(error);
}); });
it("should be create a fill that cover the query, that match perfectly with a existent view", () => {
let query = engineScenario.queryProduct;
let optimalView = engine.query(query);
expect(optimalView).to.be.an("object");
expect(optimalView).to.have.property("metrics");
expect(optimalView).to.have.property("dimensions");
expect(optimalView).to.have.property("operation");
expect(optimalView.metrics).to.be.an("array");
expect(optimalView.dimensions).to.be.an("array");
expect(optimalView.operation).to.be.an("object");
expect(optimalView.metrics).to.have.length(6);
expect(optimalView.dimensions).to.have.length(3);
expect(optimalView.operation).to.have.property("opcode");
expect(optimalView.id).to.be.equal(engineScenario.viewProduct.id);
});
it("should be create a fill that cover the query, that match perfectly with a existent view, with clauses", () => {
let query = engineScenario.queryActive;
let optimalView = engine.query(query);
expect(optimalView).to.be.an("object");
expect(optimalView).to.have.property("metrics");
expect(optimalView).to.have.property("dimensions");
expect(optimalView).to.have.property("operation");
expect(optimalView.metrics).to.be.an("array");
expect(optimalView.dimensions).to.be.an("array");
expect(optimalView.operation).to.be.an("object");
expect(optimalView.metrics).to.have.length(1);
expect(optimalView.dimensions).to.have.length(2);
expect(optimalView.operation).to.have.property("opcode");
expect(optimalView.id).to.be.equal(engineScenario.viewActiveSeller.id);
});
it("should be create a fill that cover the query, using sub dimensions", () => {
let query = engineScenario.querySubDim;
let optimalView = engine.query(query);
expect(optimalView).to.be.an("object");
expect(optimalView).to.have.property("metrics");
expect(optimalView).to.have.property("dimensions");
expect(optimalView.metrics).to.be.an("array");
expect(optimalView.dimensions).to.be.an("array");
expect(optimalView.metrics).to.have.length(0);
expect(optimalView.dimensions).to.have.length(2);
expect(optimalView).satisfy((optView: View) => {
return optView.dimensions.some((item) => item.name === subdim["subdims_day"].name);
});
expect(optimalView).satisfy((optView: View) => {
return optView.dimensions.some((item) => item.name === subdim["subdims_month"].name);
});
});
it("should throw an exception, sub-dimension with non-existent parent", () => {
let error: boolean = false;
try {
engine.query(engineScenario.queryNoParent);
}
catch (e){
error = true;
expect(e.message).to.be.equal("[Engine Error] Engine views cannot cover the query");
}
expect(error).to.be.true;
});
it("should parse a clause, with target as dimension and operator as ==", () => {
const strFilter = "dim:client:name==Laci";
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(engine.getDimensionByName("dim:client:name"));
expect(clause.filters[0].value).to.be.equal("Laci");
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:product:avg:pricein!=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(engine.getMetricByName("met:product:avg:pricein"));
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:seller:willfail==teste";
const exeption = "[Engine Error] Filter could not be created: \"dim:seller:willfail\" was not found. Check target spelling.";
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:seller:count:willfail==0";
const exeption = "[Engine Error] Filter could not be created: \"met:seller:count:willfail\" was not found. Check target spelling.";
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:sell:sum:quantity=?0";
let exeption = "[Engine Error] 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:sell:sum:quantity!?0";
exeption = "[Engine Error] 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;
});
it("should throw an exception, when a operator does not suit", () => {
const operators = [">", "<", "<=", ">="];
for (let i = 0; i < operators.length; ++i) {
let error: boolean = false;
let strFilter = "dim:client:name" + operators[i] + "Firoz";
let exeption = "[Engine Error] Filter could not be created: Operator \"" + operators[i] + "\" is invalid for target \"dim:client:name\"";
try {
engine.parseClause(strFilter);
}
catch (e){
error = true;
expect(e.message).to.be.equal(exeption);
}
expect(error).to.be.true;
}
});
it("should parse clauses with several operators for dates and integers", () => {
const operators: {[key: string]: FilterOperator} = {
">": FilterOperator.GREATER,
"<": FilterOperator.LOWER,
"<=": FilterOperator.LOWEREQ,
">=": FilterOperator.GREATEREQ,
"==": FilterOperator.EQUAL,
"!=": FilterOperator.NOTEQUAL
};
for (let op of Object.keys(operators)) {
const strFilter = "dim:sell:datein" + op + "2018-02-17";
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(engine.getDimensionByName("dim:sell:datein"));
expect(clause.filters[0].value).to.be.equal("2018-02-17");
expect(clause.filters[0].operator).to.be.equal(operators[op]);
}
for (let op of Object.keys(operators)) {
const strFilter = "dim:seller:id" + op + "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(engine.getDimensionByName("dim:seller:id"));
expect(clause.filters[0].value).to.be.equal("0");
expect(clause.filters[0].operator).to.be.equal(operators[op]);
}
});
it("should return all views", () => {
let views: View[];
views = engine.getViews();
expect(views).to.have.length(9);
for (let i = 0 ; i < views.length; i++){
expect(views[i]).to.have.property("metrics");
expect(views[i]).to.have.property("dimensions");
expect(views[i]).to.have.property("clauses");
expect(views[i]).to.have.property("origin");
expect(views[i]).to.have.property("operation");
expect(views[i]).to.have.property("id");
expect(views[i]).to.have.property("name");
}
});
it("should return null from addView", () => {
let view: View;
view = engine.addView(null);
expect(view).to.be.null;
});
it("should return null from addDimension", () => {
let dimension: Dimension;
dimension = engine.addDimension(null);
expect(dimension).to.be.null;
});
it("should return all views", () => {
let enumtype: EnumType;
let error: boolean = false;
try {
enumtype = engine.getEnumTypeByName("test");
}
catch (e){
error = true;
expect(e.message).to.be.equal("[Engine Error] EnumType: 'test' do not exist in the database." +
" Check enumtype spelling and database configuration.");
}
expect(error);
});
}); });
...@@ -18,85 +18,384 @@ ...@@ -18,85 +18,384 @@
* along with blend. If not, see <http://www.gnu.org/licenses/>. * along with blend. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Query } from "../common/query"; import { Dimension, DimensionStrOptions } from "./dimension";
import { Metric, MetricStrOptions } from "./metric";
import { Tag, TagOptions } from "../common/tag";
import { Clause } from "./clause";
import { Filter } from "./filter";
import { View } from "./view"; import { View } from "./view";
import { AggregationType } from "../common/aggregationType"; import { Query, QueryOpts } from "../common/query";
import { Graph } from "../util/graph";
import { EnumType, EnumTypeOptions} from "./enumType";
import { Source , SourceStrOptions } from "./source";
import { ViewHandler } from "../util/viewHandler";
import { ParsedConfig } from "../util/configParser";
/**
* Represents the database schema from the BlenDB perspective.
* The engine is an abstraction of a generic analytics database.
* All the operations that would be realised in the database
* are made through the engine. The main purpose of the engine
* is to translate a localition less query into a localized query.
* In other words, discover in which view a metric/dimension is.
*/
export class Engine { export class Engine {
/** Set of views available in the database */
private views: View[]; private views: View[];
/** Set of metrics available in the database. */
private metrics: Metric[];
/** Set of enumerable types available in the database. */
private enumTypes: EnumType[];
/** Set of dimensions available in the database. */
private dimensions: Dimension[];
/** Set of sources available in the database. */
private sources: Source[];
/** Set of tags available in the database. */
private tags: Tag[];
/** Graph which represents the database schema. */
private graph: Graph;
constructor (v: View[]) { /** Constructs an empty database schema. */
this.views = v; constructor (config: ParsedConfig) {
}
this.graph = new Graph();
public query (q: Query) { return this.selectOptimalView(q); }
public insert (data: any) { /*to Implement*/ } this.enumTypes = [];
private selectOptimalView (q: Query) { this.views = [];
let objective = q.metrics.concat(q.dimensions); this.metrics = [];
let optimalViews: View[] = []; this.dimensions = [];
let activeViews = this.views; this.sources = [];
this.tags = [];
// Run this block until all metrics and dimmensions are covered
while (objective.length > 0) {
let bestView: View;
let shortestDistance = objective.length + 1;
activeViews = activeViews.filter((view: View) => {
let cover = view.metrics.concat(view.dimensions);
let intersection = cover.filter((item: string) => {
return objective.indexOf(item) !== -1;
});
if (intersection.length > 0) { config.metrics.forEach ((met) => this.addMetric(met));
let distance = objective.length - intersection.length; config.dimensions.forEach ((dim) => this.addDimension(dim));
config.views.forEach ((view) => this.addView(view));
config.enumTypes.forEach ((enumt) => this.addEnumType(enumt));
config.sources.forEach ((sourc) => this.addSource(sourc));
config.tags.forEach ((tag) => this.addTag(tag));
if (distance < shortestDistance) {
bestView = view;
shortestDistance = distance;
} }
return true;
/** Gets all the available */
public getViews(): View[] {
return this.views;
}
/**
* Gets a string description for all the available metrics.
* @param expression - Expression to be used in the tags
*/
public getMetricsDescription(expression: string): MetricStrOptions[] {
let list = this.metrics.map((i) => i.strOptions());
if (!expression) {
return list;
} }
/*If the intersection is empty, const clauses = expression.split(";").filter((item: string) => item !== "");
remove this element from future searches*/
return false; for (let i = 0; i < clauses.length; ++i) {
const tags = clauses[i].split(",").filter((item: string) => item !== "");
list = list.filter((item) => {
return item.tags.some((o) => tags.some((t) => t === o));
}); });
}
if (shortestDistance === objective.length + 1) { return list;
throw new Error ("Engine views cannot cover the query");
} }
optimalViews.push(bestView); /** Gets a string description for all the available enumerable types. */
public getEnumTypesDescription(): EnumTypeOptions[] {
return this.enumTypes.map((i) => i.strOptions());
}
//remove metrics and dimensions corevered by the bestView /** Gets a string description for all the available sources. */
objective = objective.filter((item: string) => { public getSourcesDescription(): SourceStrOptions[] {
let cover = bestView.dimensions.concat(bestView.metrics); return this.sources.map((i) => i.strOptions());
return cover.indexOf(item) === -1;
});
} }
if (optimalViews.length === 1) { /**
return optimalViews.pop(); * Gets a string description for all the available dimensions.
* @param expression - Expression to be used in the tags
*/
public getDimensionsDescription(expression: string): DimensionStrOptions[] {
let list = this.dimensions.map((i) => i.strOptions());
if (!expression) {
return list;
} }
else { const clauses = expression.split(";").filter((item: string) => item !== "");
let map = new Map();
optimalViews.forEach((view: View) => { for (let i = 0; i < clauses.length; ++i) {
view.aggregationMap.forEach((value: AggregationType, key: string) => { const tags = clauses[i].split(",").filter((item: string) => item !== "");
map.set(key, value); list = list.filter((item) => {
}); return item.tags.some((o) => tags.some((t) => t === o));
}); });
}
let options = { return list;
metrics: q.metrics, }
dimensions: q.dimensions,
materialized: false,
aggregationMap: map,
childViews: optimalViews
};
let view = new View(options); /** Gets a string description for all the available tags. */
public getTagsDescription(): TagOptions[] {
return this.tags.map((i) => i.strOptions());
}
/**
* Adds a new view to the database schema (engine).
* @param view - View to be added.
*/
public addView(view: View): View {
if (this.graph.addView(view)) {
this.views.push(view); this.views.push(view);
return view; return view;
} }
return null;
}
/**
* Adds a new enumerable type to the database schema (engine).
* @param enumType - Enumerable type to be added.
*/
public addEnumType(enumType: EnumType): EnumType {
this.enumTypes.push(enumType);
return enumType;
}
/**
* Adds a new tag to the database schema (engine).
* @param enumType - Enumerable type to be added.
*/
public addTag(tag: Tag): Tag {
this.tags.push(tag);
return tag;
}
/**
* Adds a new source to the database schema (engine).
* @param source - Source to be added.
*/
public addSource(source: Source): Source {
this.sources.push(source);
return source;
}
/**
* Adds a new metric to the database schema (engine).
* @param metric - Metric to be added.
*/
public addMetric(metric: Metric): Metric {
if (this.graph.addMetric(metric)) {
this.metrics.push(metric);
return metric;
}
return null;
}
/**
* Gets the metric object given its name.
* @param name - Metric's name.
*/
public getMetricByName(name: string): Metric {
let result = this.metrics.find(metric => metric.name === name);
if (!result) {
throw new Error("[Engine Error] Metric: '" + name + "' do not exist in the database." +
" Check metric spelling and database configuration.");
}
return result;
}
/**
* Gets the enumerable type object given its name.
* @param name - Enumerable type name.
*/
public getEnumTypeByName(name: string): EnumType {
let result = this.enumTypes.find(EnumType => EnumType.name === name);
if (!result) {
throw new Error("[Engine Error] EnumType: '" + name + "' do not exist in the database." +
" Check enumtype spelling and database configuration.");
}
return result;
}
/**
* Gets the source object given its name.
* @param name - Source's name.
*/
public getSourceByName(name: string): Source {
let result = this.sources.find(source => source.name === name);
if (!result) {
throw new Error("[Engine Error] Required Source: '" + name + "' do not exist in the database." +
" Check source spelling and database configuration.");
}
return result;
}
/**
* Adds a new dimension to the database schema (engine).
* @param dimension - Dimension to be added.
*/
public addDimension(dimension: Dimension): Dimension {
if (this.graph.addDimension(dimension)) {
this.dimensions.push(dimension);
return dimension;
}
return null;
}
/**
* Gets the dimension object given its name.
* @param name - Dimension's name.
*/
public getDimensionByName(name: string): Dimension {
let result = this.dimensions.find(dimension => dimension.name === name);
if (!result) {
throw new Error("[Engine Error] Dimension: '" + name + "' do not exist in the database." +
" Check dimension spelling and database configuration.");
}
return result;
}
/**
* Translate a string clause into a clause object.
* @param strClause - Clause in string format.
*/
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});
}
/**
* Translate a string filter into a clause object.
* @param strFilter - Filter in string format.
*/
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("[Engine Error] Filter could not be created: \"" + segment.target + "\" was not found. Check target spelling.");
}
const filter = new Filter({
target: target,
operator: op,
value: segment.value
});
if (!filter.isValid) {
throw new Error("[Engine Error] Filter could not be created: Operator \"" + segment.operator + "\" is invalid for target \"" + segment.target + "\"");
}
return filter;
}
else {
throw new Error("[Engine Error] Filter could not be created: Operator on \"" + strFilter + "\" could not be extracted");
}
}
/**
* Transform a query (location less) into a view (localized).
* The main difference between a query object and a view object
* is that a view object can be built from other view objects
* and construct a valid query to conventional SGBD's.
* The query object do not have any location, so can not be
* translate in a valid SGBD query. In other words this method
* find a location (FROM clause in SQL) to the query object.
* @param q - Query to be located.
*/
public query (q: Query): View {
return this.selectOptimalView(q);
} }
/**
* Selects the best location (view) to a given query.
* @param q - Query to be located.
*/
private selectOptimalView (q: Query): View {
let queries: Query[] = [];
let qOpt: QueryOpts;
if (q.metrics.length > 0) {
for (let i = 0; i < q.metrics.length; ++i) {
qOpt = { metrics: [q.metrics[i]],
dimensions: q.dimensions,
clauses: q.clauses,
sort: q.sort };
queries.push(new Query(qOpt));
}
const views = queries.map((query) => {
return ViewHandler.growView(query, this.getCover(query));
});
return ViewHandler.queryJoin(q, views);
}
else {
let query = new Query(q);
return ViewHandler.growView(query, this.getCover(query));
}
}
/**
* Finds a set of views that has 2 properties:
* 1- Contain all the attributes (metrics/dimensions)
* required do make the query.
* 2- The set of views is connected. This means that
* if the set has more than one view is possible to take
* any view in the set exists another view that share
* at least one dimension. This meke possible to join
* views.
* @param q - Query to be covered.
*/
private getCover (q: Query): View[] {
const optimalViews = this.graph.cover(q);
if (optimalViews.length === 0) {
throw new Error("[Engine Error] Engine views cannot cover the query");
}
let matViews: View[] = optimalViews.sort((a, b) => (a.id < b.id) ? -1 : 1);
let noRepeat: View[] = [matViews[0]];
for (let i = 1; i < matViews.length; ++i) {
if (matViews[i - 1].id !== matViews[i].id) {
noRepeat.push(matViews[i]);
}
}
return noRepeat;
}
} }
/*
* Copyright (C) 2018 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/>.
*/
/** Parameters used to create a Enumerable type object. */
export interface EnumTypeOptions {
/** The type name. */
name: string;
/** The allowed values to this type. */
values: string[];
}
/**
* Defines a custom data type.
* This custom type has one restriction. Only a finite
* set o values is allowed to this type. All the
* valid values to this type are set in its definition.
*/
export class EnumType {
/** The type name. */
public name: string;
/** The allowed values to this type. */
public values: string[];
/**
* Create a enumerable type definition.
* @param options - Parameters required to create a enumerable type.
*/
constructor(options: EnumTypeOptions) {
this.name = options.name;
this.values = options.values;
}
/** Gets a string description of this enumerable type. */
public strOptions(): EnumTypeOptions {
return{
name: this.name,
values: this.values
};
}
}
/* /*
* Copyright (C) 2015 Centro de Computacao Cientifica e Software Livre * Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana * Departamento de Informatica - Universidade Federal do Parana
* *
* This file is part of blendb. * This file is part of blendb.
...@@ -20,21 +20,16 @@ ...@@ -20,21 +20,16 @@
import { expect } from "chai"; import { expect } from "chai";
import { Aggregate } from "./aggregate"; import { FilterOperator, Filter } from "./filter";
describe("aggregate class", () => { describe("filter class", () => {
it("should be instantiated with an array metrics and one of dimensions", () => { it("should correctly parse the operators", () => {
let aggr = new Aggregate(["met:one"], ["dim:one", "dim:two"]); expect(Filter.parseOperator("==")).to.be.equal(FilterOperator.EQUAL);
expect(aggr).to.be.an("object"); expect(Filter.parseOperator("!=")).to.be.equal(FilterOperator.NOTEQUAL);
}); expect(Filter.parseOperator(">")).to.be.equal(FilterOperator.GREATER);
expect(Filter.parseOperator("<")).to.be.equal(FilterOperator.LOWER);
it("should not be instantiated with an empty array of metrics", () => { expect(Filter.parseOperator(">=")).to.be.equal(FilterOperator.GREATEREQ);
let aggr = new Aggregate([], ["dim:one", "dim:two"]); expect(Filter.parseOperator("<=")).to.be.equal(FilterOperator.LOWEREQ);
expect(aggr).to.be.an("object"); expect(Filter.parseOperator("?=")).to.be.equal(FilterOperator.NONE);
});
it("should not be instantiated with an empty array of dimensions", () => {
let aggr = new Aggregate(["met:one"], []);
expect(aggr).to.be.an("object");
}); });
}); });
/*
* 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";
import { DataType } from "../common/types";
/** Parameters used to create a filter object. */
export interface FilterOptions {
/** Metric/Dimension that will be filtered. */
target: Metric|Dimension;
/** Operation applied to filter. */
operator: FilterOperator;
/** Constant value to be compared. */
value: string;
}
/**
* Parameters used to define filter object in the configuration file.
* Also the string description of a filter.
*/
export interface StrFilterOptions {
/** Metric/Dimension that will be filtered. */
target: string;
/** Operation applied to filter. */
operator: string;
/** Constant value to be compared. */
value: string;
}
/** Available operations to be used in filters */
export enum FilterOperator {
/** Used as error code. So suitable operation found. */
NONE,
/** Equality (==). */
EQUAL,
/** Not equality (!=). */
NOTEQUAL,
/** Greater than (>). */
GREATER,
/** Lower than (<). */
LOWER,
/** Greater or equal than( >=). */
GREATEREQ,
/** Lower or equal than (<=). */
LOWEREQ
}
export class Filter {
/** Hash of components that unique identify the filter. */
public readonly id: string;
/** Metric/Dimension that will be filtered. */
public readonly target: Metric|Dimension;
/** Constant value to be compared. */
public readonly operator: FilterOperator;
/** Constant value to be compared. */
public readonly value: string;
/** Constant that informs if the target type allow the operator. */
public readonly isValid: boolean;
/**
* Create a filter.
* @param options - Parameters required to create a filter.
*/
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);
this.isValid = Filter.isTypeValid(options);
}
/**
* Parse a string to enum(Filter Operator).
* @param op - Filter operator in string format.
*/
public static parseOperator(op: string): FilterOperator {
switch (op) {
case "==":
return FilterOperator.EQUAL;
case "!=":
return FilterOperator.NOTEQUAL;
case ">":
return FilterOperator.GREATER;
case "<":
return FilterOperator.LOWER;
case ">=":
return FilterOperator.GREATEREQ;
case "<=":
return FilterOperator.LOWEREQ;
default:
return FilterOperator.NONE;
}
}
/**
* Splits a continuous string in the string representation of the filter.
* @param strFilter - Filter in string format.
*/
public static segment(strFilter: string): StrFilterOptions {
for (let i = 0; i < strFilter.length; ++i) {
switch (strFilter[i]){
case "=":
if (strFilter[i + 1] === "=") {
return {
target: strFilter.slice(0, i),
operator: "==",
value: strFilter.slice(i + 2)
};
}
break;
case "!":
if (strFilter[i + 1] === "=") {
return {
target: strFilter.slice(0, i),
operator: "!=",
value: strFilter.slice(i + 2)
};
}
break;
case ">":
if (strFilter[i + 1] === "=") {
return {
target: strFilter.slice(0, i),
operator: ">=",
value: strFilter.slice(i + 2)
};
}
else {
return {
target: strFilter.slice(0, i),
operator: ">",
value: strFilter.slice(i + 1)
};
}
case "<":
if (strFilter[i + 1] === "=") {
return {
target: strFilter.slice(0, i),
operator: "<=",
value: strFilter.slice(i + 2)
};
}
else {
return {
target: strFilter.slice(0, i),
operator: "<",
value: strFilter.slice(i + 1)
};
}
default:
break;
}
}
return null;
}
/**
* Verify if the target allows the operation.
* @param op - Operator to be validated.
*/
private static isTypeValid(op: FilterOptions): boolean {
if (op.operator === FilterOperator.NONE) {
return false;
}
if (op.operator === FilterOperator.GREATER ||
op.operator === FilterOperator.LOWER ||
op.operator === FilterOperator.GREATEREQ ||
op.operator === FilterOperator.LOWEREQ) {
if (op.target.dataType === DataType.DATE ||
op.target.dataType === DataType.INTEGER) {
return true;
}
else {
return false;
}
}
else {
return true;
}
}
}