/* * 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 . */ import { View } from "../core/view"; import { Metric } from "../core/metric"; import { Dimension } from "../core/dimension"; import { Query } from "../common/query"; import { Clause } from "../core/clause"; import { FilterOperator } from "../core/filter"; enum State { UNVISITED, VISITED } /* Class (interface) for a vertex in the graph Created using a dimension or metric Several of the attributes are used just for search proposes like parent and state. */ interface Vertex { id: string; // Name of vertex neighbors: { [key: string]: Edge[] }; state: State; // During the search the vertex has a state parent: Vertex; // During the search, what vertex "find" this isSubDimension: boolean; // During insertion, was subdimension dimensionParent: Vertex; // If is a subdimension, the vertex for that dimension: Dimension; // If is a dimension, tha dimension that the vertex represents metric: Metric; // If is a metric, the metric that the vertexrepresents } /* Class (interface) dor a edge of the Graph Normaly is possible to use a list of vertices to represent the edges, however this edges have some information different from the vertex, so a class is needed. Edges represent views or parent relationship with dimensions and subdimensions. */ interface Edge { isView: boolean; // True if edge represent view view: View; // The view if isView is True subDimension: Dimension; // The subDimension if isView is False } /* Graph Class Used to convert the Views Representation (Tables/Relations) into a graph representation of the database schema. Is not generic Graph, is not possible create vertex and edges at will, only with metrics, dimensions and views. Struct: There is a vertex for each Metric and Dimension inserted in the graph. The graph is directed, but mostly vertices has edges in both directions. There is a edge between 2 vertices i and j in 3 cases: I) if i and j are in the same view, there is a edge (i, j) and (j, i) II) if i is parent of the subdimention j , there is a edge (i, j) III) If a view has only one vertex, there is a loop (i, i) So there is a edge if vertices are in the same view or there is a parent relationship between vertices. */ export class Graph { private vertices: Vertex[]; // vertices of the graph private verticeMap: { [key: string]: Vertex }; // Map where key is the vertex id and the value is the vertex public constructor () { this.vertices = []; this.verticeMap = {}; } /* Adds a metric returns true if the metric was inserted in the graph */ public addMetric(met: Metric): boolean { if (!met) { return false; } if (typeof this.verticeMap[met.name] !== "undefined") { // If the metric was already added, not add again return false; } else { let v: Vertex = { id: met.name, neighbors: {}, state: State.UNVISITED, parent: null, isSubDimension: false, dimensionParent: null, dimension: null, metric: met }; this.vertices.push(v); this.verticeMap[met.name] = v; return true; } } /* Adds a dimension returns true if the dimension was inserted in the graph */ public addDimension(dim: Dimension): boolean { if (!dim) { return false; } if (typeof this.verticeMap[dim.name] !== "undefined") { // If the dimension was already added, not add again return false; } else { if (dim.parent === null) { // If is not a subdimension, insert normaly let v: Vertex = { id: dim.name, neighbors: {}, state: State.UNVISITED, parent: null, isSubDimension: false, dimensionParent: null, dimension: dim, metric: null }; this.vertices.push(v); this.verticeMap[dim.name] = v; return true; } else { // If is a sub dimension get the parent let p = this.verticeMap[dim.parent.name]; if (typeof p === "undefined") { // If the parent was not inserted, not insert return false; } // Otherwise create a vertex and a edge for the parent // relationship. let v: Vertex = { id: dim.name, neighbors: {}, state: State.UNVISITED, parent: null, isSubDimension: true, dimensionParent: p, dimension: dim, metric: null }; this.vertices.push(v); this.verticeMap[dim.name] = v; p.neighbors[dim.name] = [{ isView: false, view: null, subDimension: dim }]; return true; } } } /* Add a View Get the metrics and dimensions of this view and create edges between then returns true when the view in inserted (create all the edges needed) and false otherwise If returns false any edge was created, if true all edges were created. */ public addView (view: View): boolean { if (!view) { return false; } let vertices = view.dimensions.map((dim) => dim.name); vertices = vertices.concat(view.metrics.map((met) => met.name)); // Concat metrics and diemnsions to get all vertices ids if (vertices.length === 1) { // If the view has only one vertex, create a loop if (this.checkEdge(vertices[0], vertices[0], view)) { this.edge(vertices[0], vertices[0], view); return true; } return false; } else { let r = true; // Check if all the edges can be created for (let i = 0; i < vertices.length; ++i) { for (let j = (i + 1); j < vertices.length; ++j) { if (r) { r = this.checkEdge(vertices[i], vertices[j], view); } } } // If at least one can not, not create any one if (!r) { return false; } // Otherwise create then all for (let i = 0; i < vertices.length; ++i) { for (let j = (i + 1); j < vertices.length; ++j) { this.edge(vertices[i], vertices[j], view); } } return true; } } /* Check if a edge can be create return false when at least one vertex does not exist in the graph or the edge already exists returns true otherwise */ private checkEdge(idV: string, idU: string, value: View): boolean { let v: Vertex = this.verticeMap[idV]; let u: Vertex = this.verticeMap[idU]; if (typeof v !== "undefined" && typeof u !== "undefined") { // Check if the vertices exist if (typeof v.neighbors[idU] !== "undefined") { // Check if a edge exist if (v.neighbors[idU].some((item) => item.view.id === value.id)) { // Check the id of the edge return false; } } return true; } else { return false; } } /* Create a edge between 2 vertices Edges are a kind of special, when say create a edge is in fact put a index on the array of a edge. The edges have a array of ids, when you try to create a edge and the edge doen not exist, create the edge and a array with a lonely index in it. If the edge exist but the id is not in the array, adds the id and checkEdge would return true, if the id already exists return false */ private edge(idV: string, idU: string, value: View) { // Assuming that checkEdge is called first let v: Vertex = this.verticeMap[idV]; let u: Vertex = this.verticeMap[idU]; if (typeof v.neighbors[idU] === "undefined") { v.neighbors[idU] = [{ isView: true, view: value, subDimension: null }]; } else { v.neighbors[idU].push({ isView: true, view: value, subDimension: null }); } if (typeof u.neighbors[idV] === "undefined") { u.neighbors[idV] = [{ isView: true, view: value, subDimension: null }]; } else { u.neighbors[idV].push({ isView: true, view: value, subDimension: null }); } } /* Given a list of metrics and dimensions returns a set of Views (children) that can be used to create a query that returns the data asked. If this set cannot be created, throws a error */ public cover(q: Query): View[] { const metrics = q.metrics; const dimensions = q.dimensions; const clauses = (q.clauses) ? q.clauses : []; let output: View[] = []; let verticesIds = this.verticesInQuery(q); for (let i = 0; i < this.vertices.length; ++i) { // Get the vertices and set their status and parent this.vertices[i].state = State.UNVISITED; this.vertices[i].parent = null; } // Check if dimension/metric is in the graph if (verticesIds.some((item) => typeof this.verticeMap[item] === "undefined")) { return []; } // Choose one vertex as root of a bfs // (Breadth-First-Search) let root: Vertex = this.verticeMap[verticesIds.shift()]; if (!root) { return []; } while (root.isSubDimension) { root = root.dimensionParent; } root.state = State.VISITED; let queue: Vertex[] = [root]; while (queue.length > 0) { let v: Vertex = queue.shift(); for (let key of Object.keys(v.neighbors)) { let u: Vertex = this.verticeMap[key]; if (this.canVisit(u, v.neighbors[key], clauses)) { // Mark all vertices visited by the search u.state = State.VISITED; u.parent = v; queue.push(u); } } } while (verticesIds.length > 0 && this.verticeMap[verticesIds[0]].state === State.VISITED) { verticesIds.shift(); } // Removes from the list all the vertices marked // If at least one vertex in not marked, the graph is // no connect and a query cannot be create if (verticesIds.length > 0) { return []; } // Recover the list of vertices let dimToCover = dimensions.map((dim) => dim); let metToCover = metrics.map((met) => met); verticesIds = this.verticesInQuery(q); while (verticesIds.length > 0) { // Choose a vertex and walks to his ancestors until // reach the root, when walking this path choose // the views based on the edges of the path and // the views already picked. let v = this.verticeMap[verticesIds.shift()]; while (v.parent !== null) { let options = v.parent.neighbors[v.id].filter((edge) => { // Filter the edges ids to get only the ones that // represent views, also get only the ones that pass // in the constraints return edge.isView && this.passConstraints(clauses, edge.view.clauses); }).map((edge) => edge.view); // Check if there is a intersection between output and options if (output.some((child) => options.some((view) => child === view))) { // If there is a intersection, does not pick any new view v = v.parent; continue; } if (options.length === 0) { // If there are no view options this means that a // subdimension edge was taken, so do not pick any // new view. v = v.parent; continue; } // Selects the best edge let bestEdge = this.pickEdge (options, metToCover, dimToCover); output.push(bestEdge); // Remove from the list of metrics and dimensions the // covered by the selected view metToCover = metToCover.filter((item) => { return !bestEdge.metrics.some((met) => met === item); }); dimToCover = dimToCover.filter((item) => { return !bestEdge.dimensions.some((dim) => dim === item); }); // walk back on the path v = v.parent; } } let views: View[] = []; // Pick all views that contain the root vertex for (let i of Object.keys(root.neighbors)) { views = views.concat(root.neighbors[i].filter((item) => { return item.isView && this.passConstraints(clauses, item.view.clauses); }).map((item) => item.view)); } // Check if some of its views were picked if (output.some((child) => views.some((view) => child === view))) { // If yes, do nothing and return the actual set return output; } // If any were picked, choose one let bestEdge = this.pickEdge (views, metToCover, dimToCover); output.push(bestEdge); // This is required for the case that only one vertex is required // Or only subdimensions edges are taken return output; } /* From a edge, coohse the best view, based on the metric and dimensions that are not cover yet, return a View. The algorithm chooses the view that covers more metrics and dimensions that are not covered yet, if there is a tie chooses the one with less dimensions, if tie again, the earliest in the list. */ private pickEdge (views: View[], metToCover: Metric[], dimToCover: Dimension[]): View { // Picks the first option as the best one until now let bestView = views[0]; let bestCoverMet = metToCover.filter((met) => { return bestView.metrics.some((item) => item === met); }); let bestCoverDim = dimToCover.filter((dim) => { let r = bestView.dimensions.some((item) => item === dim); // If is a subdimension that must be cover, check if // any ancestor can cover it while (!r && dim.parent !== null) { dim = dim.parent; r = bestView.dimensions.some((item) => item === dim); } return r; }); // Get the score let bestCover = bestCoverMet.length + bestCoverDim.length; for (let i = 1; i < views.length; ++i) { let actual = views[i]; let actualCoverMet = metToCover.filter((met) => { return actual.metrics.some((item) => item === met); }); let actualCoverDim = dimToCover.filter((dim) => { let r = actual.dimensions.some((item) => item === dim); while (!r && dim.parent !== null) { // If is a subdimension that must be cover, check if // any ancestor can cover it dim = dim.parent; r = actual.dimensions.some((item) => item === dim); } return r; }); // Checks if the new option is better than the best until now let actualCover = actualCoverMet.length + actualCoverDim.length; if (actualCover > bestCover || (actualCover === bestCover && bestView.dimensions.length > actual.dimensions.length)) { bestCover = actualCover; bestCoverMet = actualCoverMet; bestCoverDim = actualCoverDim; bestView = actual; } } return bestView; } /* Check if a vertex is can be visited from another vertex. Basically checks if the vertex is unvisited, if is a sub dimension or view edge and if is a view, chech the constraints. */ private canVisit(v: Vertex, neighbors: Edge[], clauses: Clause[] ): boolean { if (v.state !== State.UNVISITED) { return false; } for (let i = 0; i < neighbors.length; ++i) { if (neighbors[i].isView) { if (this.passConstraints(clauses, neighbors[i].view.clauses)) { return true; } } else { return true; } } return false; } /* Check if a set of filter/clauses of a view suits for the query */ private passConstraints(queryClauses: Clause[], viewClauses: Clause[]) { /* TODO: Enhance constraint check. The assumption is that the constraints are simple now and only check sub set is enought, but if the clauses get more complex then this function should be updated. Remember: The fact that a view cannot be choosed by constraints only means that a more inneficient view must be choosen. */ return viewClauses.every((view) => queryClauses.some((query) => { if (view.id === query.id) { return true; } else if (query.filters.length === 1 && view.filters.length === 1) { let queryFilter = query.filters[0]; let viewFilter = view.filters[0]; if (queryFilter.target.name !== viewFilter.target.name) { return false; } let queryValue: number; let viewValue: number; if (queryFilter.target.dataType === "date") { queryValue = new Date(queryFilter.value).getTime(); viewValue = new Date(viewFilter.value).getTime(); } else { queryValue = parseFloat(queryFilter.value); viewValue = parseFloat(viewFilter.value); } if (viewFilter.operator === FilterOperator.GREATEREQ && queryValue >= viewValue ) { if (queryFilter.operator === FilterOperator.GREATER || queryFilter.operator === FilterOperator.GREATEREQ) { return true; } } else if (viewFilter.operator === FilterOperator.GREATER && queryValue > viewValue ) { if (queryFilter.operator === FilterOperator.GREATER || queryFilter.operator === FilterOperator.GREATEREQ) { return true; } } else if (viewFilter.operator === FilterOperator.LOWEREQ && queryValue <= viewValue ) { if (queryFilter.operator === FilterOperator.LOWER || queryFilter.operator === FilterOperator.LOWEREQ) { return true; } } else if (viewFilter.operator === FilterOperator.LOWER && queryValue < viewValue ) { if (queryFilter.operator === FilterOperator.LOWER || queryFilter.operator === FilterOperator.LOWEREQ) { return true; } } } return false; })); } private verticesInQuery(q: Query): string[] { const metrics = q.metrics; const dimensions = q.dimensions; const clauses = (q.clauses) ? q.clauses : []; let verticesIds = metrics.map((met) => met.name); verticesIds = verticesIds.concat(dimensions.map((dim) => dim.name)); for (let i = 0; i < clauses.length; ++i) { verticesIds = verticesIds.concat(clauses[i].filters.map((filter) => { return filter.target.name; })); } const sorted = verticesIds.sort(); if (sorted.length > 0) { const unique = [sorted[0]]; for (let i = 1; i < sorted.length; ++i) { if (sorted[i - 1] !== sorted[i]) { unique.push(sorted[i]); } } return unique; } else { return []; } } }