diff --git a/src/adapter/postgres.spec.ts b/src/adapter/postgres.spec.ts index 70ab8478fe58c925df0a58714b09365d1b27a8e3..80167f09c0f07aeb46645ae8dd476ef32cb7e7e9 100644 --- a/src/adapter/postgres.spec.ts +++ b/src/adapter/postgres.spec.ts @@ -357,4 +357,23 @@ describe("postgres adapter", () => { done(); }); }); + + it("should get data from view when joins can be propagated", (done) => { + let view = adapterScenario.propagatedClauseView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(2); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + done(); + }); + }); }); diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts index 4b8d63fc5c9127de093df08c4ec9c284a18125cd..f2c7a3532b01168d01ab75540b3868fc815143a8 100644 --- a/src/adapter/postgres.ts +++ b/src/adapter/postgres.ts @@ -112,6 +112,7 @@ export class PostgresAdapter extends Adapter { 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 @@ -158,6 +159,17 @@ export class PostgresAdapter extends Adapter { } } + /* + 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]; + } + } + } + for (let i = 0; i < partialJoin.length; ++i) { const dims = partialJoin[i].dimensions.filter((item) => { return map[item.name] > 1; @@ -171,13 +183,32 @@ export class PostgresAdapter extends Adapter { again, with less dimensions, removing this dimension from the view. */ - if (dims.length < partialJoin[i].dimensions.length) { + + 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; + 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: partialJoin[i].clauses, + clauses: coveredClauses.concat(partialJoin[i].clauses), materialized: false }); const from = "(" + diff --git a/src/core/clause.ts b/src/core/clause.ts index 11a831dc55cc90349305958189d17d79175cecb0..8f61c5ddb2083a3399cb9cfc393cacf82a4b8511 100644 --- a/src/core/clause.ts +++ b/src/core/clause.ts @@ -20,6 +20,8 @@ import { Filter } from "./filter"; import { Hash } from "../util/hash"; +import { Dimension } from "./dimension"; +import { Metric } from "./metric"; export interface ClauseOptions { filters: Filter[]; @@ -28,11 +30,29 @@ export interface ClauseOptions { export class Clause { public readonly id: string; public readonly filters: Filter[]; + public readonly targets: (Metric|Dimension)[]; constructor (options: ClauseOptions) { - this.filters = options.filters; + this.filters = options.filters.map((i) => i); + 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]); + } + } + } + else { + this.targets = []; + } const filtersIds = this.filters.map((item) => item.id); this.id = Hash.sha1(filtersIds.sort()); } + public isCovered(coverage: (Metric|Dimension)[]): boolean { + return coverage.every((i) => this.targets.some((j) => i.name === j.name)); + } } diff --git a/test/scenario.ts b/test/scenario.ts index 856cc9ae37687beec4bb7c3b3a36260a4a0e92dc..946f1b7c33e4bf2c52dab0721adf3c1561d4e17c 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -54,6 +54,7 @@ interface AdapterScenario { notOriginCount: View; unMaterializebleView: View; partialJoinView: View; + propagatedClauseView: View; } interface DataCtrlScenario { @@ -117,6 +118,11 @@ const filters: { [key: string]: Filter } = { operator: FilterOperator.NOTEQUAL, value: "1" }), + "dim:4" : new Filter({ + target: dims[4], + operator: FilterOperator.NOTEQUAL, + value: "dim:4:1" + }), "dim:5" : new Filter({ target: dims[5], operator: FilterOperator.NOTEQUAL, @@ -137,6 +143,7 @@ const clauses: { [key: string]: Clause } = { "view0le": new Clause({filters: [filters["dim:0:le"]]}), "view0dim0": new Clause({filters: [filters["dim:0:0"], filters["dim:0:1"]]}), "view9dim2": new Clause({filters: [filters["dim:2"]]}), + "view6dim4": new Clause({filters: [filters["dim:4"]]}), "view7dim5": new Clause({filters: [filters["dim:5"]]}) }; @@ -348,6 +355,15 @@ const partialJoinView = new View({ childViews: [views[3], views[5], views[6]] }); +const propagatedClauseView = new View({ + metrics: [mets[8]], + dimensions: [dims[4]], + materialized: false, + origin: false, + childViews: [views[6], views[7]], + clauses: [clauses.view7dim5, clauses.view6dim4] +}); + export const engineScenario: EngineScenario = { metrics: mets, dimensions: dims, @@ -374,7 +390,8 @@ export const adapterScenario: AdapterScenario = { notMatchFilterView: notMatchFilterView, notOriginCount: notOriginCount, unMaterializebleView: unMaterializebleView, - partialJoinView: partialJoinView + partialJoinView: partialJoinView, + propagatedClauseView: propagatedClauseView }; export const dataCtrlScenario: DataCtrlScenario = {