diff --git a/src/adapter/sql.ts b/src/adapter/sql.ts index 244863a3d75ed938e69d3250402bcdb4347fdb41..3600c1b81b0184357d88f5f723fd92fd61149aed 100644 --- a/src/adapter/sql.ts +++ b/src/adapter/sql.ts @@ -199,7 +199,7 @@ export abstract class SQLAdapter extends Adapter { materialized: false }); const from = "(" + - this.buildQuery(partial, [partialJoin[i]]) + + this.buildQuery(partial, [partialJoin[i]], false) + ") AS view_" + partial.id + "\n"; partialJoin[i].id = partial.id; @@ -302,7 +302,7 @@ export abstract class SQLAdapter extends Adapter { materialized: false }); const viewFrom = "(" + - this.buildQuery(partial, segment[i]) + + this.buildQuery(partial, segment[i], false) + ") AS view_" + partial.id + "\n"; partialJoin.push({ @@ -507,7 +507,7 @@ export abstract class SQLAdapter extends Adapter { materialized: false }).id; const viewFrom = "(" + - this.buildQuery(partial, [partial0, partial1]) + + this.buildQuery(partial, [partial0, partial1], false) + ") AS view_" + id + "\n"; partialJoin.push({ id: id, @@ -532,7 +532,7 @@ export abstract class SQLAdapter extends Adapter { layer to the query, that is in fact unnecessary. Think a way to remove-it. */ - return this.buildQuery(view, partialJoin) + ";"; + return this.buildQuery(view, partialJoin, true) + ";"; } private searchMaterializedViews(view: View): View[] { @@ -551,10 +551,11 @@ export abstract class SQLAdapter extends Adapter { return r; } - private buildQuery(target: View, views: ExpandedView[]) { + private buildQuery(target: View, views: ExpandedView[], toSort: boolean) { const metrics = target.metrics; const dimensions = target.dimensions; const clauses = target.clauses; + const sort = target.sort; let dimMap: {[key: string]: DimInfo} = {}; let nameMap: {[key: string]: ExpandedView} = {}; @@ -655,6 +656,11 @@ export abstract class SQLAdapter extends Adapter { } }); + // Sorting + const order = sort.map((item) => { + return "\"" + item.name + "\""; + }).join(","); + // Assembly const projection = "SELECT " + elements.join(","); @@ -664,8 +670,9 @@ export abstract class SQLAdapter extends Adapter { if (grouped.length > 0) { grouping = " GROUP BY " + grouped.join(","); } + const sorting = (toSort && sort.length > 0) ? " ORDER BY " + order : ""; - return projection + source + selection + grouping; + return projection + source + selection + grouping + sorting; } diff --git a/src/api/controllers/data.spec.ts b/src/api/controllers/data.spec.ts index 5cf78079cdf2b0e85ef8479b6b69e12dca38416f..4719012ee537f87792ecde6a13cf1e39296d4c56 100644 --- a/src/api/controllers/data.spec.ts +++ b/src/api/controllers/data.spec.ts @@ -29,6 +29,7 @@ interface StrQuery { metrics: string; dimensions: string; filters?: string; + sort?: string; } function parseQuery(obj: Query): StrQuery { @@ -80,6 +81,27 @@ describe("API data controller", () => { .end(done); }); + it("should respond 500 when query has sort item that is not in query data", (done) => { + let query = parseQuery(tests.clausal); + query.sort = "dim:0"; + request(server) + .get("/v1/data") + .query(query) + .expect(500) + .expect((res: any) => { + const message = "Query execution failed: " + + "Could not construct query with the paramters given."; + const error = "The item 'dim:0'" + + " is not present in neither metrics nor dimensions list"; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("message"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.eql(message); + expect(res.body.error).to.be.eql(error); + }) + .end(done); + }); + it("should respond 200 and get some data", (done) => { request(server) .get("/v1/data") @@ -176,4 +198,31 @@ describe("API data controller", () => { .end(done); }); + it("should respond 200 and get some data, sorted", (done) => { + // Clause does not come to scenario besause is a lot of work for + // only a single test + let query = parseQuery(tests.clausal); + query.sort = "dim:7,met:0"; + request(server) + .get("/v1/data") + .query(query) + .expect(200) + .expect((res: any) => { + let result = res.body; + expect(result).to.be.an("array"); + expect(result).to.have.length(5); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(tests.clausal.metrics.map((item) => item.name)); + keys = keys.concat(tests.clausal.dimensions.map((item) => item.name)); + for (let i = 0; i < result.length; ++i) { + const row = result[i]; + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + expect(row["dim:7"]).to.be.eql(i + 1); + } + }) + .end(done); + }); + }); diff --git a/src/api/controllers/data.ts b/src/api/controllers/data.ts index 154adb99ae9e1df2f838d0c5e798632eb41970c9..add1317f681f8701b1b5b3991a2cd6c883342119 100644 --- a/src/api/controllers/data.ts +++ b/src/api/controllers/data.ts @@ -27,13 +27,17 @@ export class DataCtrl { let metrics = req.query.metrics.split(",").filter((item: string) => item !== ""); let dimensions = req.query.dimensions.split(",").filter((item: string) => item !== ""); let clauses = []; + let sort: string[] = []; if (req.query.filters) { clauses = req.query.filters.split(";").filter((item: string) => item !== ""); } + if (req.query.sort) { + sort = req.query.sort.split(",").filter((item: string) => item !== ""); + } let view; try { - let query: Query = { metrics: [], dimensions: [], clauses: [] }; + let query: Query = { metrics: [], dimensions: [], clauses: [], sort: [] }; for (let i = 0; i < metrics.length; ++i) { query.metrics.push(req.engine.getMetricByName(metrics[i])); } @@ -45,6 +49,26 @@ export class DataCtrl { for (let i = 0; i < clauses.length; ++i) { query.clauses.push(req.engine.parseClause(clauses[i])); } + + for (let i = 0; i < sort.length; ++i) { + const m = query.metrics.find((item) => item.name === sort[i]); + if (!m) { + const d = query.dimensions.find((item) => item.name === sort[i]); + if (!d) { + throw new Error( + "The item '" + sort[i] + + "' is not present in neither metrics nor dimensions list"); + } + else { + query.sort.push(d); + } + } + + else { + query.sort.push(m); + } + + } view = req.engine.query(query); } catch (e) { diff --git a/src/common/query.ts b/src/common/query.ts index dcbcb1f24cbec0d4ef85b45859facf519f85c4f8..fd7f646e012dbd213a65fe5c0949e79fa83827b6 100644 --- a/src/common/query.ts +++ b/src/common/query.ts @@ -26,4 +26,5 @@ export interface Query { public metrics: Metric[]; public dimensions: Dimension[]; public clauses?: Clause[]; + public sort?: (Metric|Dimension)[]; } diff --git a/src/core/engine.ts b/src/core/engine.ts index e1d842cfa473d950ed185a33b934bdb9600e911c..c359596cb1d43892b96284ecfc0426391c76b878 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -163,13 +163,16 @@ export class Engine { 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)) { + perfectMatch(optimalViews[0].clauses, clauses) && + perfectOrder(optimalViews[0].sort, sort)) { return optimalViews[0]; } else { @@ -177,6 +180,7 @@ export class Engine { metrics: metrics, dimensions: dimensions, clauses: clauses, + sort: sort, materialized: false, origin: false, // Never a dynamic generated view will be origin childViews: optimalViews @@ -196,8 +200,20 @@ export class Engine { } function perfectMatch (array1: Clause[], - array2: Clause[]) { + array2: Clause[]): boolean { return array1.every((item: Clause) => { return array2.some((otherItem: Clause) => item.id === otherItem.id); }); } + +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; + } + } + + return true; +} diff --git a/src/core/view.ts b/src/core/view.ts index 7634a8fe8e8548c520286a7185b37ead7f7a8942..29e6a63551e02ccfa64ffe858b2203815a72c7a9 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -34,6 +34,7 @@ export interface ViewOptions { keys?: Dimension[]; origin: boolean; clauses?: Clause[]; + sort?: (Metric|Dimension)[]; materialized?: boolean; childViews?: View[]; } @@ -44,6 +45,7 @@ export class View { 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[]; @@ -52,6 +54,7 @@ export class View { 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 : [];