diff --git a/src/adapter/postgres.spec.ts b/src/adapter/postgres.spec.ts index 2279807dbebaf119f1645df35d6614d52aa6a381..d9c5a2d71e7d649b890a350f79c407f0d1693489 100644 --- a/src/adapter/postgres.spec.ts +++ b/src/adapter/postgres.spec.ts @@ -350,7 +350,7 @@ describe("postgres adapter", () => { 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).to.have.length(2); expect(result[0]).to.be.an("object"); let keys: string[] = []; keys = keys.concat(view.metrics.map((item) => item.name)); @@ -369,7 +369,7 @@ describe("postgres adapter", () => { 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).to.have.length(1); expect(result[0]).to.be.an("object"); let keys: string[] = []; keys = keys.concat(view.metrics.map((item) => item.name)); diff --git a/src/adapter/sql.ts b/src/adapter/sql.ts index e3b2d89c29878bb52722c89c7fb1885ffe3b63ed..4c10cd3168401f38a9808b1c03de297517f47f78 100644 --- a/src/adapter/sql.ts +++ b/src/adapter/sql.ts @@ -25,22 +25,12 @@ import { Dimension } from "../core/dimension"; import { Clause } from "../core/clause"; import { Filter, FilterOperator } from "../core/filter"; import { AggregationType, RelationType, DataType } from "../common/types"; +import { Operation, Opcode } from "../common/expression"; import { View } from "../core/view"; -interface ExpandedView { - dimensions: Dimension[]; - metrics: Metric[]; - dimMetrics: Metric[]; - keys: Dimension[]; - clauses: Clause[]; - from: string; - id: string; - origin: boolean; -} - interface DimInfo { dim: Dimension; - views: ExpandedView[]; + views: View[]; } interface DimTranslation { @@ -50,516 +40,152 @@ interface DimTranslation { expanded: boolean; } +interface QueryAndName { + query: string; + name: string; +} + +interface DimAndNameMap { + dimensions: {[key: string]: DimInfo}; + views: {[key: string]: View}; +} + export abstract class SQLAdapter extends Adapter { public getQueryFromView(view: View): string { - /* - Find the base (materialized) views that has this data and exapand - than (make a parse to the format used in the adapter) - */ - const materialized = this.searchMaterializedViews(view).sort((a, b) => { - return (a.id < b.id) ? -1 : 1; - }).map((item) => { - return { - id: item.id, - from: "view_" + item.id, - dimMetrics: [], - metrics: item.metrics.filter((i) => { - return view.metrics.some((j) => i.name === j.name); - }), - dimensions: item.dimensions, - keys: item.keys, - clauses: item.clauses, - origin: item.origin - }; + const partials = this.buildPartials(view).filter((i) => { + return i.query !== ""; + }).map((i) => { + return i.name + " AS (" + i.query + ")"; }); - // Remove repeated views from the result - let partialJoin = [materialized[0]]; - for (let i = 1; i < materialized.length; ++i) { - if (materialized[i - 1].id !== materialized[i].id) { - partialJoin.push(materialized[i]); - } + let withClause = ""; + if (partials.length > 0) { + withClause = "WITH " + partials.join(", ") + " "; } - const blackList = view.dimensions.map((i) => i.name); - for (let i = 0; i < view.clauses.length; ++i) { - if (view.clauses[i].filters.length === 1) { - let filter = view.clauses[i].filters[0]; - if (filter.operator === FilterOperator.EQUAL) { - blackList.push(filter.target.name); - } - } - } + let sort = ""; + if (view.sort.length > 0) { + // Sorting + const order = view.sort.map((item) => { + return "\"" + item.name + "\""; + }).join(","); - /* - If there is more than one source of data (tables/views) - a join is needed. - - Partial Join represents how many sources still exists, - every join reduces this number. - */ - let clausesToCover = view.clauses.map((i) => i); - while (partialJoin.length > 1) { - /* - Variable map finds what dimenensions are still needed to - complete this query, they are required for 2 reasons. - 1 - To make joins - 2 - Because they are in the query - - For each view that has this dimension we add one score to - this dimension, if they are in the query the same. - - Automatically if the dimension is in the query there will be - at least one view with this atribute (or the query could not be - completed) so dimensions in the query always have score of - at least 2. - - To make a join the dimension must be in 2 different views, - creating a score of 2 either. - - If the score is less than 2 so this dimension is not required - anymore and can be removed. - */ - let map: { [key: string]: number } = {}; - let partialsChange = false; - for (let i = 0; i < partialJoin.length; ++i) { - const dims = partialJoin[i].dimensions; - for (let k = 0; k < dims.length; ++k) { - if (!map[dims[k].name]) { - map[dims[k].name] = 1; - } + sort = " ORDER BY " + order; + } - else { - ++map[dims[k].name]; - } - } - } + return withClause + "SELECT * FROM " + this.viewName(view) + sort + ";"; + } - for (let i = 0; i < view.dimensions.length; ++i) { - let dim = view.dimensions[i]; - while (dim !== null) { - if (map[dim.name]) { - ++map[dim.name]; - } - dim = dim.parent; + private buildPartials(view: View): QueryAndName[] { + let op = view.operation; + let queue: View[] = op.values.map((i) => i); + const output: QueryAndName[] = [{ + query: this.operation(op, view), + name: this.viewName(view) + }]; + + const map: {[key: string]: boolean } = {}; + + while (queue.length > 0) { + const partial = queue.shift(); + if (!map[partial.id]) { + const query = this.operation(partial.operation, partial); + if (query !== "") { + map[partial.id] = true; + output.unshift({ + query: query, + name: this.viewName(partial) + }); + queue = queue.concat(partial.operation.values); } } + } - /* - Also mark scores for dimensions inside clauses - */ - for (let i = 0; i < clausesToCover.length; ++i) { - for (let j = 0; j < clausesToCover[i].targets.length; ++j) { - if (map[clausesToCover[i].targets[j].name]) { - ++map[clausesToCover[i].targets[j].name]; - } - } - } + return output; + } - for (let i = 0; i < partialJoin.length; ++i) { - const dims = partialJoin[i].dimensions.filter((item) => { - return map[item.name] > 1; - }); - const keys = partialJoin[i].keys.filter((item) => { - return map[item.name] > 1; - }); - /* - At this point the dimensions with less than score 2 - are removed, if this happens the view is agreggated - again, with less dimensions, removing this dimension - from the view. - */ - - let coveredClauses: Clause[] = []; - let notCoveredClauses: Clause[] = []; - /* - If all dimensions in a clause are a sub set of the - dimensions of a view, this clause is apllied now, - propagating the clause to this point. - - Then this clause is removed from the set of clauses - */ - for (let j = 0; j < clausesToCover.length; ++j) { - if (clausesToCover[j].isCovered(partialJoin[i].dimensions)) { - coveredClauses.push(clausesToCover[j]); - } - else { - notCoveredClauses.push(clausesToCover[j]); - } - } - clausesToCover = notCoveredClauses.filter((clause) => { - return !partialJoin[i].clauses.some((c) => c.id === clause.id); - }); - if (dims.length < partialJoin[i].dimensions.length || coveredClauses.length > 0) { - const partial = new View({ - metrics: partialJoin[i].metrics, - dimensions: dims, - keys: keys, - origin: false, - clauses: coveredClauses.concat(partialJoin[i].clauses), - materialized: false - }); - const from = "(" + - this.buildQuery(partial, [partialJoin[i]], false) + - ") AS view_" + partial.id + "\n"; + protected viewName(view: View): string { + return "view_" + view.id; + } - partialJoin[i].id = partial.id; - partialJoin[i].dimensions = partial.dimensions; - partialJoin[i].keys = partial.keys; - partialJoin[i].origin = partial.origin; - partialJoin[i].from = from; + private operation(op: Operation, view: View): string { + switch (op.opcode) { + case Opcode.REDUCE: + return this.buildOperation(view, op.values, false); + case Opcode.JOIN: + return this.buildOperation(view, op.values, true); + default: + // Is unnecessary make a push function. Push = materialized + // No need for materialized partials + return ""; + } + } - partialsChange = true; - } + private buildOperation(view: View, partials: View[], isJoin: boolean): string { + // Mapping, which views the metrics and dimensions are + const mapping = this.buildMaps(partials); + // Projection + const metrics = view.metrics.map((i) => { + const sourceView = mapping.views[i.name]; + return this.translateMetric(i, sourceView); + }); + const dimensions = view.dimensions.map((dimension) => { + let dim = dimension; + while (!mapping.views[dim.name]) { + dim = dim.parent; } - /* - If at least one of the views changed (have the number of - dimensions reduced) returns to the begining of the loop - again. - - Othewise we need to make a join. - */ - if (!partialsChange) { - /* - Sorting the views by keys. - If the keys are identical, then they - will be in sequence, and views with identical - keys can be joined. - - Sort an array of keys is the same as sort a - array of strings. - */ - const sorted = partialJoin.sort((a, b) => { - return this.compareKeys(a.keys, b.keys, blackList); - }); - /* - SUPER WARNING: WHEN THE BLACK LIST IS USED THE VIEW IS - UNMATERIALIZEBLE, BUT THE QUERY CAN AGGREGATE THE VALUES - - The blackList is the array of dimensions of the query plus - the dimensions in filters using the equality operator. - In further coments is expained that the relation to make - a join must be one-to-one between the tables. - However and a dimension is choosed, a sub view is - created and if the relation is preserved in the sub view - the query can be agregated, but this view cannot be re-used - so it is unmaterializeble. - - The equality operator is the same as select one subview. - */ - /* - First of all, the remaining views are splited in segments. - - A segment contains views with the same keys that are great - to make joins. Joins like this do not create "dimensional - metrics". - - In joins like this one row of each view will be connected - with at most one row of each other table. - */ - const segment = [[sorted[0]]]; - let segmentId = 0; - for (let i = 1; i < sorted.length; ++i) { - if (this.compareKeys(sorted[i - 1].keys, sorted[i].keys, blackList) === 0) { - segment[segmentId].push(sorted[i]); - } - else { - ++segmentId; - segment.push([sorted[i]]); - } - } - - partialJoin = []; - let ableToJoin = false; - - for (let i = 0; i < segment.length; ++i) { - /* - If a segment has more than one view, a join can be made - */ - if (segment[i].length > 1) { - let mets: Metric[] = []; - let clauses: Clause[] = []; - let dims: Dimension[] = []; - let dimMetrics: Metric[] = []; - for (let j = 0; j < segment[i].length; ++j) { - mets = mets.concat(segment[i][j].metrics); - clauses = clauses.concat(segment[i][j].clauses); - dims = dims.concat(segment[i][j].dimensions); - dimMetrics = dimMetrics.concat(segment[i][j].dimMetrics); - } - - dims = this.removeDuplicatedDimensions(dims); - /* - Its atributes are just concatenated and the - duplicates removed. - */ - - const partial = new View({ - metrics: mets, - dimensions: dims, - keys: segment[i][0].keys, - origin: false, - clauses: clauses, - materialized: false - }); - const viewFrom = "(" + - this.buildQuery(partial, segment[i], false) + - ") AS view_" + partial.id + "\n"; - - partialJoin.push({ - id: partial.id, - from: viewFrom, - dimMetrics: dimMetrics, - metrics: partial.metrics, - dimensions: partial.dimensions, - keys: partial.keys, - clauses: partial.clauses, - origin: partial.origin - }); - - ableToJoin = true; - } - - else { - /* - If the segment has just one view, anything can be - done at this point, so just reinsert this view in - set of views. - */ - partialJoin.push(segment[i][0]); - } - } - - /* - If at least one join was made in the last part (a segment - with more than one view) than return to the begining of the - loop. - - This permits after a join remove the dimensions that were - only choosen to this join, and are no longer required - - Ideally the joins should be restrict the join method used - above, but in some cases this can not be done. - - So if all the segments have only one view inside, move - to the next method. - */ - if (!ableToJoin) { - /* - At this point 2 views will be joined, first the - similarity with each pair of views is calculated, - the pair with the biggedt similarity will be joined. - - Similarity is calculated with the number of common - dimensions in the keys. - */ - let similarity = 0; - let idx0 = 0; - let idx1 = 1; - for (let i = 0; i < partialJoin.length; ++i) { - for (let j = i + 1 ; j < partialJoin.length; ++j) { - const pi = partialJoin[i].keys; - const pj = partialJoin[j].keys; - let score = this.similarDimensions (pi, pj); - if (similarity < score) { - similarity = score; - idx0 = i; - idx1 = j; - } - } - } + const sourceView = mapping.views[dim.name]; + return this.translateDimension(dimension, dim, sourceView); + }); + const projElem = dimensions.map((i) => i.aliased).concat(metrics); - const partial0 = partialJoin[idx0]; - const partial1 = partialJoin[idx1]; - - partialJoin.splice(idx1, 1); - partialJoin.splice(idx0, 1); - - /* - Once the views are select they are joined with the - same method, concatenedted its atributes and - removing duplicates, however the nasty effect of - this join is the creation of "dimensional metrics". - - "Dimensional metrics" are metrics that can no longer - be aggregated, and at this point to the end - of a query they will act as dimensions. - - This change happens to avoid inconsistency generated - by a join where one row of one table can be connected - to more than one of other table. - - Take this example. - - View0 : metrics [met0], dimensions [dim0] - values: [{met0: 10, dim0: 1}] - View1 : metrics [met1], dimensions [dim2] - values: [{met1: 10, dim2: 1}. {met1: 5, dim2: 2}] - View2 : metrics [], dimensions [dim0, dim1, dim2] - values: [ - {dim0: 1, dim1: 1, dim2: 1}, - {dim0: 1, dim1: 1, dim2: 2} - ] - The query is metrics [met0, met1] and dimensions [dim1] - First a join of View0 and View1 is made, the result - is: [ - {dim0: 1, dim1: 1, dim2: 1, met0: 10}, - {dim0: 1, dim1: 1, dim2: 2, met0: 10} - ] - Note that the value of met0 is duplicated. - Now dim0 is removed, than joined with view2 resulting - in: [ - {met1: 10, dim1: 1, dim2: 1, met0: 10}, - {met1: 5 , dim1: 1, dim2: 2, met0: 10} - ] - - Lets assume that the agregation is SUM - If we remove dim2 and re-agregate the result is: [ - {met1: 15, dim1: 1, met0: 20} - ] - - This result is wrong. The replication of the value - met0 affects the result. - - See if met1 was not required, first the dimemnsion would - be reduced, left dim0 and dim1, than joined that reduced - again resulting in the value [ - {dim1:1, met0: 10} - ] - - Is this case there is no duplication and the aggregation - does not include more rows than should. - - To solve this problem the met0 must become a dimension, - in other words, not aggregated again. If the met0 was - not agregated in the query met0, met1, dim1 the result - is: [ - {met1: 15, dim1: 1, met0: 10} - ] - what is compatible. - - After this extreme long explanation what must be - known is: Joining views with diferent keys - generate "dimensional metrics". - - Views with "dimensional metrics" can not used for future - queries because can not be re-agregated, so this must be - avoided and is one-query only views. - */ - - let dimMetrics: Metric[]; - let mets: Metric[]; - let dims = partial0.dimensions.concat(partial1.dimensions); - dims = this.removeDuplicatedDimensions(dims); - let keys = partial0.keys.concat(partial1.keys); - keys = this.removeDuplicatedDimensions(keys); - if (partial0.keys.length === similarity) { - /* - Here the metrics become dimensions, but the effect - can be reduced. If the keys of partial0 - is a sub set of the keys ou partial1 - than the number of rows of partial 1 is not - affected, in other words the metrics of partial1 - can be aggregated and does not need to become - dimensions. - */ - partial0.dimMetrics = partial0.dimMetrics.concat(partial0.metrics); - partial0.metrics = []; - mets = partial1.metrics; - } + // Grouping + const grouped = dimensions.map((item) => { + return (item.expanded) ? item.alias : item.noalias; + }); - else if (partial1.keys.length === similarity) { - /* - The same occurs if the keys of partia1 is a subset - of partial0. - */ - partial1.dimMetrics = partial1.dimMetrics.concat(partial1.metrics); - partial1.metrics = []; - mets = partial0.metrics; - } + // Selection + const conds = []; + const clauses = this.orphanClauses(view, partials); + for (let i = 0; i < clauses.length; ++i) { + const trClause = this.translateClause(clauses[i], mapping.views); + if (trClause) { + conds.push("(" + trClause + ")"); + } + } - else { - /* - But if there is no sub set, than both sides have - the metrics turned in dimensions. - */ - partial0.dimMetrics = partial0.dimMetrics.concat(partial0.metrics); - partial0.metrics = []; - partial1.dimMetrics = partial1.dimMetrics.concat(partial1.metrics); - partial1.metrics = []; - mets = []; + // Joinning + if (isJoin) { + const dimMap = mapping.dimensions; + for (let i of Object.keys(dimMap)) { + if (dimMap[i].views.length > 1) { + const views = dimMap[i].views; + const dim = dimMap[i].dim; + const leftSide = this.buildColumn(dim, views.shift().id); + while (views.length > 0) { + const rightSide = this.buildColumn(dim, views.shift().id); + conds.push("(" + leftSide + "=" + rightSide + ")"); } - - dimMetrics = partial0.dimMetrics.concat(partial1.dimMetrics); - const partial = new View({ - metrics: mets, - dimensions: dims, - keys: keys, - origin: false, - clauses: partial0.clauses.concat(partial1.clauses), - materialized: false - }); - const id = new View({ - metrics: mets.concat(dimMetrics), - dimensions: dims, - keys: keys, - origin: false, - clauses: partial0.clauses.concat(partial1.clauses), - materialized: false - }).id; - const viewFrom = "(" + - this.buildQuery(partial, [partial0, partial1], false) + - ") AS view_" + id + "\n"; - partialJoin.push({ - id: id, - from: viewFrom, - dimMetrics: dimMetrics, - metrics: mets, - dimensions: dims, - keys: keys, - clauses: partial.clauses, - origin: false - }); - } } } - /* - When only one view remain, the query is made and a ; - is added at the end. - - TODO: Probrably this last line adds one more - layer to the query, that is in fact unnecessary. - Think a way to remove-it. - */ - return this.buildQuery(view, partialJoin, true) + ";"; - } - - private searchMaterializedViews(view: View): View[] { - let r: View[] = []; - if (view.materialized) { - return [view]; - } + // Assembly + const projection = "SELECT " + projElem.join(", "); + const sources = "FROM " + partials.map((i) => this.viewName(i)).join(", "); + const selection = (conds.length > 0) ? " WHERE " + conds.join(" AND ") : ""; - else { - let children = view.childViews; - for (let i = 0; i < children.length; ++i) { - r = r.concat(this.searchMaterializedViews(children[i])); - } + let grouping = ""; + if (grouped.length > 0) { + grouping = " GROUP BY " + grouped.join(","); } - return r; + return projection + sources + selection + grouping; } - private buildQuery(target: View, views: ExpandedView[], toSort: boolean): string { - const metrics = target.metrics; - const dimensions = target.dimensions; - const clauses = target.clauses; - const sort = target.sort; - + private buildMaps(views: View[]): DimAndNameMap { let dimMap: {[key: string]: DimInfo} = {}; - let nameMap: {[key: string]: ExpandedView} = {}; + let nameMap: {[key: string]: View} = {}; for (let i = 0; i < views.length; ++i) { const mets = views[i].metrics; @@ -586,95 +212,20 @@ export abstract class SQLAdapter extends Adapter { } } - // Projection - const strMetrics = metrics.map((metric) => { - const view = nameMap[metric.name]; - if (view) { - return this.translateMetric(metric, view); - } - - return ""; - }).filter((item) => item !== ""); - - const parsedDimensions = dimensions.map((dimension) => { - let dim = dimension; - while (!nameMap[dim.name]) { - dim = dim.parent; - } - const view = nameMap[dim.name]; - return this.translateDimension(dimension, dim, view); - }); - - let parsedDimMetrics: DimTranslation[] = []; - - for (let i = 0; i < views.length; ++i) { - const dimMets = views[i].dimMetrics.map((item) => { - return this.translateDimMetric(item, views[i]); - }); - - parsedDimMetrics = parsedDimMetrics.concat(dimMets); - } - - const totalDimensions = parsedDimensions.concat(parsedDimMetrics); - - const strDimensions = totalDimensions.map ((item) => item.aliased); - const grouped = totalDimensions.map((item) => { - return (item.expanded) ? item.alias : item.noalias; - }); - const elements = strMetrics.concat(strDimensions); - - // Joins - let conds: string[] = []; - for (let i of Object.keys(dimMap)) { - let remainViews = dimMap[i].views.slice(); - let dim = dimMap[i].dim; - let leftSide = this.buildColumn(dim, remainViews.shift().id); - if (remainViews.length > 0) { - while (remainViews.length > 0) { - const id = remainViews.shift().id; - const rightSide = this.buildColumn(dim, id); - conds.push(leftSide + " = " + rightSide); - } - } - - } - - // Selection - let covered: Clause[] = []; - for (let i = 0; i < views.length; ++i) { - // Get the clauses that children already cover - covered = covered.concat(views[i].clauses); - } - - const toCover = clauses.filter((item) => !covered.some ((clause) => { - return clause.id === item.id; - })); - - toCover.forEach((item) => { - const clause = "(" + this.translateClause(item, nameMap) + ")"; - if (clause !== "()") { - conds.push(clause); - } - }); - - // Sorting - const order = sort.map((item) => { - return "\"" + item.name + "\""; - }).join(","); - - // Assembly + return { + dimensions: dimMap, + views: nameMap + }; + } - const projection = "SELECT " + elements.join(","); - const source = " FROM " + views.map((view) => view.from).join(","); - const selection = (conds.length > 0) ? " WHERE " + conds.join(" AND ") : ""; - let grouping = ""; - if (grouped.length > 0) { - grouping = " GROUP BY " + grouped.join(","); + private orphanClauses(view: View, partials: View[]): Clause[] { + let parentClauses: Clause[] = []; + for (let i = 0; i < partials.length; ++i) { + parentClauses = parentClauses.concat(partials[i].clauses); } - const sorting = (toSort && sort.length > 0) ? " ORDER BY " + order : ""; - - return projection + source + selection + grouping + sorting; + // return clauses that does not exist in the partials + return view.clauses.filter((i) => !parentClauses.some((j) => j.id === i.id)); } private getAggregateFunction(aggrType: AggregationType, origin: boolean): string { @@ -732,7 +283,7 @@ export abstract class SQLAdapter extends Adapter { return "view_" + id + "." + quotedName; } - private translateClause(clause: Clause, map: {[key: string]: ExpandedView}): string { + private translateClause(clause: Clause, map: {[key: string]: View}): string { const r = clause.filters.map((item) => { return this.translateFilter(item, map); }).filter((item) => { @@ -741,7 +292,7 @@ export abstract class SQLAdapter extends Adapter { return r.join(" OR "); } - private translateFilter(filter: Filter, map: {[key: string]: ExpandedView}): string { + private translateFilter(filter: Filter, map: {[key: string]: View}): string { if (!map[filter.target.name]) { return ""; } @@ -753,27 +304,16 @@ export abstract class SQLAdapter extends Adapter { return this.applyOperator(leftSide, castedValue, filter.operator); } - private translateMetric(metric: Metric, view: ExpandedView): string { + private translateMetric(metric: Metric, view: View): string { const func = this.getAggregateFunction(metric.aggregation, view.origin); const quotedName = "\"" + metric.name + "\""; const extMetric = func + "(" + this.buildColumn(metric, view.id) + ")"; return extMetric + " AS " + quotedName; } - private translateDimMetric(metric: Metric, view: ExpandedView): DimTranslation { - const quotedName = "\"" + metric.name + "\""; - const extMetric = this.buildColumn(metric, view.id); - return { - aliased: extMetric + " AS " + quotedName, - noalias: extMetric, - alias: quotedName, - expanded: false - }; - } - private translateDimension(dimension: Dimension, ancestor: Dimension, - view: ExpandedView): DimTranslation { + view: View): DimTranslation { const quotedName = "\"" + dimension.name + "\""; let extDimension = this.buildColumn(ancestor, view.id); let aux = dimension; @@ -791,61 +331,6 @@ export abstract class SQLAdapter extends Adapter { }; } - protected abstract applyOperator(leftSide: string, rightSide: string, op: FilterOperator): string; - - protected abstract typeCast(quotedValue: string, dt: DataType): string; - - private compareKeys(a: Dimension[], b: Dimension[], blackList: string[]): number { - /* - SUPER WARNING: WHEN THE BLACK LIST IS USED THE VIEW IS - UNMATERIALIZEBLE, BUT THE QUERY CAN AGGREGATE THE VALUES - */ - let c = a.filter((i) => !blackList.some((bad) => bad === i.name)); - let d = b.filter((i) => !blackList.some((bad) => bad === i.name)); - let length = 0; - let res = c.length - d.length; - if (c.length < d.length) { - length = c.length; - } - else { - length = d.length; - } - - for (let i = 0; i < length; ++i) { - if (c[i].name < d[i].name) { - return -1; - } - - else if (c[i].name > d[i].name) { - return 1; - } - } - - return res; - } - - private similarDimensions(a: Dimension[], b: Dimension[]): number { - let count = 0; - for (let i = 0; i < a.length; ++i) { - if (b.some((itemB) => a[i].name === itemB.name)) { - count++; - } - } - return count; - } - - private removeDuplicatedDimensions(candidateDims: Dimension[]): Dimension[] { - let filterDims: { [key: string]: boolean } = {}; - const dims = []; - for (let i = 0; i < candidateDims.length; ++i) { - if (!filterDims[candidateDims[i].name]) { - dims.push(candidateDims[i]); - filterDims[candidateDims[i].name] = true; - } - } - return dims; - } - public getQueryFromSource(source: Source, data: any[]): string { let consult: string; let colums: any[] = []; @@ -863,4 +348,8 @@ export abstract class SQLAdapter extends Adapter { return consult; } + + protected abstract applyOperator(leftSide: string, rightSide: string, op: FilterOperator): string; + + protected abstract typeCast(quotedValue: string, dt: DataType): string; } diff --git a/src/common/expression.ts b/src/common/expression.ts new file mode 100644 index 0000000000000000000000000000000000000000..513203e49a174da879617d657bd190c71fdef28c --- /dev/null +++ b/src/common/expression.ts @@ -0,0 +1,32 @@ +/* + * 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"; + +export enum Opcode { + PUSH, + JOIN, + REDUCE +} + +export interface Operation { + opcode: Opcode; + values: View[]; +} diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index b326b88303bef93c1223563f4a604ff51a9f9542..03098f5eca2970e098880a908e4a0bfcc23f032c 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -50,10 +50,8 @@ describe("engine class", () => { expect(optimalView).to.be.an("object"); expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("dimensions"); - expect(optimalView).to.have.property("childViews"); expect(optimalView.metrics).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array"); - expect(optimalView.childViews).to.be.an("array"); expect(optimalView.metrics).to.have.length(12); expect(optimalView.dimensions).to.have.length(12); }); @@ -92,13 +90,13 @@ describe("engine class", () => { expect(optimalView).to.be.an("object"); expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("dimensions"); - expect(optimalView).to.have.property("childViews"); + expect(optimalView).to.have.property("operation"); expect(optimalView.metrics).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array"); - expect(optimalView.childViews).to.be.an("array"); + expect(optimalView.operation).to.be.an("object"); expect(optimalView.metrics).to.have.length(4); expect(optimalView.dimensions).to.have.length(2); - expect(optimalView.childViews).to.have.length(0); + expect(optimalView.operation).to.have.property("opcode"); expect(optimalView.id).to.be.equal(views[0].id); }); @@ -120,15 +118,13 @@ describe("engine class", () => { expect(optimalView).to.be.an("object"); expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("dimensions"); - expect(optimalView).to.have.property("childViews"); - expect(optimalView).to.have.property("materialized"); + expect(optimalView).to.have.property("operation"); expect(optimalView.metrics).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array"); - expect(optimalView.childViews).to.be.an("array"); + expect(optimalView.operation).to.be.an("object"); expect(optimalView.metrics).to.have.length(3); expect(optimalView.dimensions).to.have.length(1); - expect(optimalView.childViews).to.have.length(0); - expect(optimalView.materialized).to.be.true; + expect(optimalView.operation).to.have.property("opcode"); expect(optimalView.id).to.be.equal(views[9].id); }); @@ -143,13 +139,10 @@ describe("engine class", () => { expect(optimalView).to.be.an("object"); expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("dimensions"); - expect(optimalView).to.have.property("childViews"); expect(optimalView.metrics).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array"); - expect(optimalView.childViews).to.be.an("array"); expect(optimalView.metrics).to.have.length(0); expect(optimalView.dimensions).to.have.length(2); - expect(optimalView.childViews).to.have.length(3); expect(optimalView).satisfy((optView: View) => { return optView.dimensions.some((item) => item.name === subdim[0].name); @@ -169,13 +162,10 @@ describe("engine class", () => { expect(optimalView).to.be.an("object"); expect(optimalView).to.have.property("metrics"); expect(optimalView).to.have.property("dimensions"); - expect(optimalView).to.have.property("childViews"); expect(optimalView.metrics).to.be.an("array"); expect(optimalView.dimensions).to.be.an("array"); - expect(optimalView.childViews).to.be.an("array"); expect(optimalView.metrics).to.have.length(0); expect(optimalView.dimensions).to.have.length(2); - expect(optimalView.childViews).to.have.length(3); expect(optimalView).satisfy((optView: View) => { return optView.dimensions.some((item) => item.name === subdim[2].name); diff --git a/src/core/engine.ts b/src/core/engine.ts index 8058722d32e8bd36011f6541c1ad714ea5daf9c8..72a554a47eff5f3e9dac364367647855f02e9ece 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -27,6 +27,16 @@ import { Query } from "../common/query"; import { Graph } from "../util/graph"; import { EnumType, EnumTypeOptions} from "./enumType"; import { Source , SourceStrOptions } from "./source"; +import { ViewHandler } from "../util/viewHandler"; + +interface Score { + [key: string]: number; +} + +interface ViewsAndClauses { + views: View[]; + clauses: Clause[]; +} export class Engine { private views: View[] = []; @@ -197,67 +207,51 @@ export class Engine { } private selectOptimalView (q: Query): View { - let optimalViews = this.graph.cover(q); - if (optimalViews.length === 0) { - throw new Error ("Engine views cannot cover the query"); - } + let queries: Query[] = []; + if (q.metrics.length > 0) { + for (let i = 0; i < q.metrics.length; ++i) { + queries.push({ + metrics: [q.metrics[i]], + dimensions: q.dimensions, + clauses: (q.clauses) ? q.clauses : [], + sort: (q.sort) ? q.sort : [] + }); + } + const views = queries.map((query) => { + return ViewHandler.growView(query, this.getCover(query)); + }); - // If all the metrics and dimensions are the same and only exist one child view - // return this single child view - const metrics = q.metrics; - const dimensions = q.dimensions; - const clauses = ((q.clauses) ? q.clauses : []); - const sort = ((q.sort) ? q.sort : []); - if (optimalViews.length === 1 && - optimalViews[0].metrics.length === metrics.length && - optimalViews[0].dimensions.length === dimensions.length && - optimalViews[0].clauses.length === clauses.length && - optimalViews[0].sort.length === sort.length && - optimalViews[0].metrics.every((item) => metrics.indexOf(item) !== -1) && - optimalViews[0].dimensions.every((item) => dimensions.indexOf(item) !== -1) && - perfectMatch(optimalViews[0].clauses, clauses) && - perfectOrder(optimalViews[0].sort, sort)) { - return optimalViews[0]; + return ViewHandler.queryJoin(q, views); } + else { - let options = { - metrics: metrics, - dimensions: dimensions, - clauses: clauses, - sort: sort, - materialized: false, - origin: false, // Never a dynamic generated view will be origin - childViews: optimalViews + let query = { + metrics: q.metrics, + dimensions: q.dimensions, + clauses: (q.clauses) ? q.clauses : [], + sort: (q.sort) ? q.sort : [] }; - - let view = new View(options); - // this.addView(view); - /* - This line has been removed for now because not all views can be - re-used by other views (unmaterializeble views), and so far this - is only detected in the adapter, when this can be detected in - engine, than the queries can be added again to the engine - */ - return view; + return ViewHandler.growView(query, this.getCover(query)); } + } -} -function perfectMatch (array1: Clause[], - array2: Clause[]): boolean { - return array1.every((item: Clause) => { - return array2.some((otherItem: Clause) => item.id === otherItem.id); - }); -} + private getCover (q: Query): View[] { + const optimalViews = this.graph.cover(q); + if (optimalViews.length === 0) { + throw new Error ("Engine views cannot cover the query"); + } -function perfectOrder (array1: (Metric|Dimension)[], - array2: (Metric|Dimension)[]): boolean { - // Assuming that the arrays have the same length - for (let i = 0; i < array1.length; ++i) { - if (array1[i].name !== array2[i].name) { - return false; + 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 true; + return noRepeat; + + } } diff --git a/src/core/view.ts b/src/core/view.ts index 29e6a63551e02ccfa64ffe858b2203815a72c7a9..b1cba66060f122b4bb0af42c240f0c6102070b11 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -22,6 +22,7 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; import { Hash } from "../util/hash"; import { Clause } from "./clause"; +import { Operation, Opcode } from "../common/expression"; export interface LoadView { view: View; @@ -31,38 +32,36 @@ export interface LoadView { export interface ViewOptions { metrics: Metric[]; dimensions: Dimension[]; - keys?: Dimension[]; origin: boolean; clauses?: Clause[]; sort?: (Metric|Dimension)[]; - materialized?: boolean; - childViews?: View[]; + operation?: Operation; } export class View { public readonly id: string; public readonly metrics: Metric[]; public readonly dimensions: Dimension[]; - public readonly keys: Dimension[]; public readonly clauses: Clause[]; public readonly sort: (Metric|Dimension)[]; - public readonly materialized: boolean; public readonly origin: boolean; - public childViews: View[]; + public readonly operation: Operation; constructor (options: ViewOptions) { this.metrics = options.metrics.sort(); this.dimensions = options.dimensions.sort(); this.clauses = (options.clauses) ? options.clauses.sort() : []; this.sort = (options.sort) ? options.sort : []; - this.materialized = options.materialized || false; this.origin = options.origin || false; - this.childViews = (options.childViews) ? options.childViews : []; - if (options.keys && options.keys.length > 0) { - this.keys = options.keys.sort(); + if (options.operation) { + this.operation = options.operation; } + else { - this.keys = this.dimensions; + this.operation = { + opcode: Opcode.PUSH, + values: [] + }; } // calculate the id of the view based on it's metrics and dimensions diff --git a/src/util/configParser.ts b/src/util/configParser.ts index 8ce6c3520c5befa67c77cc854c6f24f41ec22efb..87f8ef6e25290307891f3b8f73d2977358e2e090 100644 --- a/src/util/configParser.ts +++ b/src/util/configParser.ts @@ -23,6 +23,7 @@ import { Dimension, DimensionOptions, DimensionStrOptions } from "../core/dimens import { View, ViewOptions, LoadView } from "../core/view"; import { EnumType, EnumTypeOptions } from "../core/enumType"; import { RelationType, DataType } from "../common/types"; +import { Opcode } from "../common/expression"; import { Filter } from "../core/filter"; import { Clause } from "../core/clause"; import { Source, SourceOptions, SourceStrOptions} from "../core/source"; @@ -38,7 +39,6 @@ export interface ViewParsingOptions { dimensions: string[]; metrics: string[]; clauses?: string[]; - keys?: string[]; } interface ConfigSchema { @@ -222,15 +222,14 @@ export class ConfigParser { let viewOpt: ViewOptions = { metrics: [], dimensions: [], - materialized: true, origin: opts.origin, - childViews: [], clauses: [], - keys: [] + operation: { + opcode: Opcode.PUSH, + values: [] + } }; - const keys = (opts.keys) ? opts.keys : []; - for (let i = 0; i < opts.metrics.length; ++i) { if (metMap[opts.metrics[i]]) { viewOpt.metrics.push(metMap[opts.metrics[i]]); @@ -251,15 +250,6 @@ export class ConfigParser { throw new Error("[Parsing error] Non exist dimension set to view " + opts.alias); } } - for (let i = 0; i < keys.length; ++i) { - if (dimMap[keys[i]]) { - viewOpt.keys.push(dimMap[opts.keys[i]]); - } - - else { - throw new Error("[Parsing error] Non exist key set to view " + opts.alias); - } - } if (opts.clauses) { for (let i = 0; i < opts.clauses.length; ++i) { diff --git a/src/util/graph.spec.ts b/src/util/graph.spec.ts index 72e372e09888ec204203d6a49ac1c87413cb951e..8dc8ea982468a6e32235da45507d7dae24220dc8 100644 --- a/src/util/graph.spec.ts +++ b/src/util/graph.spec.ts @@ -96,20 +96,17 @@ describe("graph class", () => { new View({ metrics: [], dimensions: [dims[0], dims[1]], - origin: true, - materialized: true + origin: true }), new View({ metrics: [], dimensions: [dims[2], dims[3]], - origin: true, - materialized: true + origin: true }), new View({ metrics: [], dimensions: dims, - origin: true, - materialized: true + origin: true }) ]; @@ -133,8 +130,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: [dims[0], dims[1]], - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.true; @@ -155,8 +151,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: dims, - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.false; @@ -177,8 +172,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: [dim], - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.true; @@ -195,8 +189,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: [dim], - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.true; @@ -231,38 +224,32 @@ describe("graph class", () => { new View({ metrics: [mets[0]], dimensions: [dims[0]], - origin: true, - materialized: true + origin: true }), new View({ metrics: [mets[1]], dimensions: [dims[1]], - origin: true, - materialized: true + origin: true }), new View({ metrics: [mets[2]], dimensions: [dims[2]], - origin: true, - materialized: true + origin: true }), new View({ metrics: [], dimensions: dims, - origin: true, - materialized: true + origin: true }), new View({ metrics: mets, dimensions: dims, - origin: true, - materialized: true + origin: true }), new View({ metrics: [mets[0], mets[1]], dimensions: [dims[0], dims[1]], - origin: true, - materialized: true + origin: true }), ]; @@ -304,8 +291,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: [dims[0]], - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.true; @@ -344,8 +330,7 @@ describe("graph class", () => { let view = new View({ metrics: [], dimensions: [dims[0]], - origin: true, - materialized: true + origin: true }); expect(g.addView(view)).to.be.true; @@ -385,23 +370,19 @@ describe("graph class", () => { let view1 = new View({ metrics: [], dimensions: [dims[0], dims[1]], - origin: true, - materialized: true + origin: true }); let view2 = new View({ metrics: [], dimensions: [dims[1], dims[2]], - origin: true, - materialized: true + origin: true }); let view3 = new View({ metrics: [], dimensions: dims, origin: false, - materialized: false, - childViews: [view1, view2], clauses: [clause1] }); @@ -517,7 +498,6 @@ describe("graph class", () => { metrics: [], dimensions: dims, origin: false, - materialized: true, clauses: clauses }); @@ -630,8 +610,7 @@ describe("graph class", () => { new View({ metrics: [], dimensions: dims, - origin: false, - materialized: true + origin: false }) ]; @@ -640,7 +619,6 @@ describe("graph class", () => { metrics: [], dimensions: dims, origin: false, - materialized: true, clauses: [clauses[i]] })); } diff --git a/src/util/viewHandler.ts b/src/util/viewHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e3aea11348b477f03048b3d805dc240e5f5b5d6 --- /dev/null +++ b/src/util/viewHandler.ts @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2018 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 { Opcode } from "../common/expression"; +import { View } from "../core/view"; +import { Clause } from "../core/clause"; +import { Query } from "../common/query"; +import { Dimension } from "../core/dimension"; +import { Metric } from "../core/metric"; + +interface Score { + [key: string]: number; +} + +interface ViewsAndClauses { + views: View[]; + clauses: Clause[]; +} + +export class ViewHandler { + public static queryJoin(q: Query, views: View[]): View { + if (views.length > 1) { + return new View({ + metrics: q.metrics, + dimensions: q.dimensions, + origin: false, + clauses: (q.clauses) ? q.clauses : [], + sort: (q.sort) ? q.sort : [], + operation: { + opcode: Opcode.JOIN, + values: views.map((i) => i) + } + }); + } + + else { + return views[0]; + } + } + + public static queryReduce(q: Query, view: View): View { + const v = new View({ + metrics: q.metrics, + dimensions: q.dimensions, + origin: false, + clauses: (q.clauses) ? q.clauses : [], + sort: (q.sort) ? q.sort : [], + operation: { + opcode: Opcode.REDUCE, + values: [view] + } + }); + + return (v.id !== view.id) ? v : view; + } + + public static growView(q: Query, views: View[]): View { + let clausesToCover = q.clauses.map((i) => i); + let metView: View = null; + let partialJoin: View[] = null; + let reduced: ViewsAndClauses = null; + let partialQuery: Query = { + metrics: q.metrics, + dimensions: q.dimensions, + clauses: q.clauses, + sort: q.sort + }; + + partialJoin = views.map((i) => (i)); + if (q.metrics.length === 0) { // ignore metView if there are 0 metrics + while (partialJoin.length > 1) { + partialQuery.clauses = clausesToCover; + reduced = ViewHandler.applyReduce(partialJoin, partialQuery); + clausesToCover = reduced.clauses; + partialJoin = ViewHandler.applyJoin(reduced.views); + } + + return ViewHandler.applyReduce(partialJoin, partialQuery).views[0]; + } + + else { + const metViewId = partialJoin.findIndex((i) => i.metrics.some((j) => { + return j.name === q.metrics[0].name; + })); + + metView = partialJoin[metViewId]; + partialJoin.splice(metViewId, 1); + while (partialJoin.length > 1) { + partialJoin.push(metView); // MetView is now the last view + partialQuery.clauses = clausesToCover; + reduced = ViewHandler.applyReduce(partialJoin, partialQuery); + clausesToCover = reduced.clauses; + metView = reduced.views.pop(); + partialJoin = ViewHandler.applyJoin(reduced.views); + } + + if (partialJoin.length === 0) { + return ViewHandler.applyReduce([metView], partialQuery).views[0]; + } + + else { + partialJoin.push(metView); + // In this case, there are exactly 2 views + reduced = ViewHandler.applyReduce(partialJoin, partialQuery); + partialJoin = ViewHandler.applyJoin(reduced.views); + partialQuery.clauses = reduced.clauses; + return ViewHandler.applyReduce(partialJoin, partialQuery).views[0]; + } + } + } + + private static dimensionsScore(views: View[], clauses: Clause[], dimensions: Dimension[]): Score { + let map: { [key: string]: number } = {}; + for (let i = 0; i < views.length; ++i) { + const dims = views[i].dimensions; + for (let k = 0; k < dims.length; ++k) { + if (map[dims[k].name]) { + ++map[dims[k].name]; + } + + else { + map[dims[k].name] = 1; + } + } + } + + for (let i = 0; i < dimensions.length; ++i) { + let dim = dimensions[i]; + if (map[dim.name]) { + ++map[dim.name]; + } + + else { // Necessary for sub dimensions + map[dim.name] = 1; + } + } + + /* + Also mark scores for dimensions inside clauses + */ + for (let i = 0; i < clauses.length; ++i) { + for (let j = 0; j < clauses[i].targets.length; ++j) { + if (map[clauses[i].targets[j].name]) { + ++map[clauses[i].targets[j].name]; + } + } + } + return map; + } + + private static applyReduce(v: View[], q: Query): ViewsAndClauses { + let changed = true; + const views = v.map((i) => i); + let clausesToCover = q.clauses.map((i) => i); + while (changed) { + changed = false; + let map = ViewHandler.dimensionsScore(views, clausesToCover, q.dimensions); + const unfilledSubDims = q.dimensions.filter((item) => { + return map[item.name] < 2; + }); + let ancestors: { [key: string]: Dimension[] } = {}; + for (let i = 0; i < unfilledSubDims.length; ++i) { + let dim = unfilledSubDims[i]; + ancestors[dim.name] = [dim]; + dim = dim.parent; + while (dim !== null) { + ancestors[unfilledSubDims[i].name].push(dim); + dim = dim.parent; + } + } + for (let i = 0; i < views.length; ++i) { + const dims = views[i].dimensions.filter((item) => { + return map[item.name] > 1; + }); + /* + At this point the dimensions with less than score 2 + are removed, if this happens the view is agreggated + again, with less dimensions, removing this dimension + from the view. + */ + + for (let key of Object.keys(ancestors)) { + const intersection = (ancestors[key].some((anc) => { + return views[i].dimensions.some((dim) => { + return dim.name === anc.name; + }); + })); + + if (intersection) { + // The first item is always the sub dimension + dims.push(ancestors[key][0]); + } + } + + let coveredClauses: Clause[] = []; + let notCoveredClauses: Clause[] = []; + /* + If all dimensions in a clause are a sub set of the + dimensions of a view, this clause is apllied now, + propagating the clause to this point. + + Then this clause is removed from the set of clauses + */ + for (let j = 0; j < clausesToCover.length; ++j) { + if (clausesToCover[j].isCovered(views[i].dimensions)) { + coveredClauses.push(clausesToCover[j]); + } + else { + notCoveredClauses.push(clausesToCover[j]); + } + } + + clausesToCover = notCoveredClauses.filter((clause) => { + return !views[i].clauses.some((c) => c.id === clause.id); + }); + + // Intersection between query metrics and view metrics + const mets = q.metrics.filter((met) => { + return views[i].metrics.some((m) => m.name === met.name); + }); + + if (dims.length < views[i].dimensions.length || coveredClauses.length > 0) { + const partial = ViewHandler.queryReduce({ + metrics: mets, + dimensions: dims, + clauses: coveredClauses.concat(views[i].clauses), + sort: q.sort + }, views[i]); + + views[i] = partial; + changed = true; + } + } + } + + return { + views: views, + clauses: clausesToCover + }; + } + + private static applyJoin(v: View[]): View[] { + /* + At this point 2 views will be joined, first the + similarity with each pair of views is calculated, + the pair with the biggedt similarity will be joined. + + Similarity is calculated with the number of common + dimensions in the keys. + */ + const views = v.map((i) => i); + let similarity = 0; + let idx0 = 0; + let idx1 = 1; + for (let i = 0; i < views.length; ++i) { + for (let j = i + 1 ; j < views.length; ++j) { + const pi = views[i].dimensions; + const pj = views[j].dimensions; + let score = this.similarDimensions (pi, pj); + if (similarity < score) { + similarity = score; + idx0 = i; + idx1 = j; + } + } + } + + const partial0 = views[idx0]; + const partial1 = views[idx1]; + + views.splice(idx1, 1); + views.splice(idx0, 1); + + let dims = partial0.dimensions.concat(partial1.dimensions); + dims = ViewHandler.removeDuplicatedDimensions(dims); + + let mets = partial0.metrics.concat(partial1.metrics); + mets = ViewHandler.removeDuplicatedMetrics(mets); + + let clauses = partial0.clauses.concat(partial1.clauses); + clauses = ViewHandler.removeDuplicatedClauses(clauses); + + const partialQuery: Query = { + metrics: mets, + dimensions: dims, + clauses: clauses + }; + + const partial = ViewHandler.queryJoin(partialQuery, [partial0, partial1]); + views.push(partial); + return views; + } + + private static similarDimensions(a: Dimension[], b: Dimension[]): number { + let count = 0; + for (let i = 0; i < a.length; ++i) { + if (b.some((itemB) => a[i].name === itemB.name)) { + count++; + } + } + return count; + } + + private static removeDuplicatedDimensions(candidateDims: Dimension[]): Dimension[] { + let filterDims: { [key: string]: boolean } = {}; + const dims = []; + for (let i = 0; i < candidateDims.length; ++i) { + if (!filterDims[candidateDims[i].name]) { + dims.push(candidateDims[i]); + filterDims[candidateDims[i].name] = true; + } + } + return dims; + } + + private static removeDuplicatedMetrics(candidateMets: Metric[]): Metric[] { + let filterMets: { [key: string]: boolean } = {}; + const mets = []; + for (let i = 0; i < candidateMets.length; ++i) { + if (!filterMets[candidateMets[i].name]) { + mets.push(candidateMets[i]); + filterMets[candidateMets[i].name] = true; + } + } + return mets; + } + + private static removeDuplicatedClauses(candidateClauses: Clause[]): Clause[] { + let filterClauses: { [key: string]: boolean } = {}; + const clauses = []; + for (let i = 0; i < candidateClauses.length; ++i) { + if (!filterClauses[candidateClauses[i].id]) { + clauses.push(candidateClauses[i]); + filterClauses[candidateClauses[i].id] = true; + } + } + return clauses; + } + } diff --git a/test/scenario.ts b/test/scenario.ts index 2e1bd1b85f8924846f2ede10a6f064cc1a5162a3..59a91b727ebcf6bda866c754c093360a6ba50251 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -26,6 +26,7 @@ import { Filter, FilterOperator } from "../src/core/filter"; import { Clause } from "../src/core/clause"; import { AggregationType, RelationType , DataType} from "../src/common/types"; import { Query} from "../src/common/query"; +import { ViewHandler } from "../src/util/viewHandler"; interface EngineScenario { metrics: Metric[]; @@ -215,158 +216,135 @@ const dateSubDim = [ }) ]; -const dateView = new View({ +const dateView = ViewHandler.queryReduce({ metrics: [], dimensions: dateSubDim, - materialized: false, - origin: false, - childViews: [views[0]] -}); +}, views[0]); -const aggrView = new View({ +const aggrView = ViewHandler.queryJoin({ metrics: [mets[0], mets[1], mets[6], mets[10], mets[11]], dimensions: [], - materialized: false, - origin: false, - childViews: [views[0], views[2], views[3]] -}); - -const clauseView = new View({ +}, [ + ViewHandler.queryReduce({ + metrics: [mets[0], mets[1], mets[10]], + dimensions: [] + }, views[0]), + ViewHandler.queryReduce({ + metrics: [mets[6], mets[11]], + dimensions: [] + }, views[2]) +]); + +const clauseView = ViewHandler.queryReduce({ metrics: [mets[0], mets[1], mets[2]], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view0dim7] -}); +}, views[0]); -const multiFilterView = new View({ +const multiFilterView = ViewHandler.queryReduce({ metrics: [mets[0], mets[1]], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view0dim0] -}); +}, views[0]); -const multiClauseView = new View({ +const multiClauseView = ViewHandler.queryReduce({ metrics: [mets[0], mets[1]], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view0dim0, clauses.view0dim7] -}); +}, views[0]); -const notEqualView = new View({ +const notEqualView = ViewHandler.queryReduce({ metrics: [], dimensions: [dims[4], dims[5]], - materialized: false, - origin: false, - childViews: [views[7]], clauses: [clauses.view7dim5] -}); +}, views[7]); -const gtltView = new View({ +const gtltView = ViewHandler.queryReduce({ metrics: [], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view0gt, clauses.view0lt] -}); +}, views[0]); -const geleView = new View({ +const geleView = ViewHandler.queryReduce({ metrics: [], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view0ge, clauses.view0le] -}); +}, views[0]); -const notMatchFilterView = new View({ +const notMatchFilterView = ViewHandler.queryReduce({ metrics: [mets[0]], dimensions: [dims[0]], - materialized: false, - origin: false, - childViews: [views[0]], clauses: [clauses.view7dim5] -}); +}, views[0]); -const subDimView = new View({ +const subDimView = ViewHandler.queryJoin({ metrics: [mets[0]], - dimensions: [subdims[0], subdims[1], dims[7], dims[8]], - materialized: false, - origin: false, - childViews: [views[0], views[1], views[4]] -}); - -const join4View = new View({ + dimensions: [subdims[0], subdims[1], dims[7], dims[8]] +}, [ + ViewHandler.queryReduce({ + metrics: [mets[0]], + dimensions: [subdims[0], dims[7]] + }, views[0]), + ViewHandler.queryJoin({ + metrics: [], + dimensions: [subdims[1], dims[7], dims[8]] + }, [views[1], views[4]]) +]); + +const join4View = ViewHandler.queryJoin({ metrics: [mets[0], mets[1], mets[2], mets[3], mets[4], mets[5]], - dimensions: [dims[2], dims[7], dims[8]], - materialized: false, - origin: false, - childViews: [views[0], views[1], views[2], views[4]] -}); + dimensions: [dims[2], dims[7], dims[8]] +}, [views[0], views[1], views[2], views[4]]); -const noSelView = new View({ +const noSelView = ViewHandler.queryJoin({ metrics: [mets[0], mets[3]], - dimensions: [], - materialized: false, - origin: false, - childViews: [views[0], views[1]] -}); + dimensions: [] +}, [views[0], views[1]]); -const withSelView = new View({ +const withSelView = ViewHandler.queryJoin({ metrics: [mets[0], mets[1]], - dimensions: [dims[7], dims[8]], - materialized: false, - origin: false, - childViews: [views[0], views[4]] -}); + dimensions: [dims[7], dims[8]] +}, [views[0], views[4]]); const notOriginCount = new View({ metrics: [mets[5], mets[6], mets[7]], dimensions: [dims[2]], - materialized: true, origin: false, clauses: [clauses.view9dim2] }); -const unMaterializebleView = new View({ +const unMaterializebleView = ViewHandler.queryJoin({ metrics: [mets[7], mets[9]], - dimensions: [dims[5]], - materialized: false, - origin: false, - childViews: [views[3], views[5], views[7], views[8]] -}); - -const partialJoinView = new View({ + dimensions: [dims[5]] +}, +[ViewHandler.queryReduce({ + metrics: [mets[9]], + dimensions: [dims[5]] +}, views[8]), +ViewHandler.queryReduce({metrics: [mets[7]], dimensions: [dims[5]]}, +ViewHandler.queryJoin({metrics: [mets[7]], dimensions: [dims[3], dims[5]]}, [ +ViewHandler.queryReduce({metrics: [], dimensions: [dims[3], dims[5]]}, +ViewHandler.queryJoin({metrics: [], dimensions: [dims[3], dims[4], dims[5]]}, [ +ViewHandler.queryReduce({metrics: [], dimensions: [dims[4], dims[5]]}, views[7]), +ViewHandler.queryReduce({metrics: [], dimensions: [dims[3], dims[4]]}, views[3]) +])), views[5]]))]); + +const partialJoinView = ViewHandler.queryJoin({ metrics: [mets[7], mets[8]], - dimensions: [], - materialized: false, - origin: false, - childViews: [views[3], views[5], views[6]] -}); + dimensions: [] +}, [views[3], views[5], views[6]]); -const propagatedClauseView = new View({ +const propagatedClauseView = ViewHandler.queryJoin({ metrics: [mets[8]], dimensions: [dims[4]], - materialized: false, - origin: false, - childViews: [views[6], views[7]], clauses: [clauses.view7dim5, clauses.view6dim4] -}); +}, [views[6], views[7]]); -const propagatedClauseAggrView = new View({ +const propagatedClauseAggrView = ViewHandler.queryJoin({ metrics: [mets[8], mets[5]], dimensions: [dims[2]], - materialized: false, - origin: false, - childViews: [views[9], views[6], views[7]], clauses: [clauses.view7dim5, clauses.view6dim4, clauses.view9dim2] -}); +}, [views[9], views[6], views[7]]); export const engineScenario: EngineScenario = { metrics: mets,