Issue #55: Add sorting to API

Signed-off-by: Lucas Fernandes de Oliveira's avatarLucas Fernandes de Oliveira <lfo14@inf.ufpr.br>
parent d85a585e
Pipeline #14889 passed with stages
in 2 minutes and 1 second
......@@ -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;
}
......
......@@ -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);
});
});
......@@ -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) {
......
......@@ -26,4 +26,5 @@ export interface Query {
public metrics: Metric[];
public dimensions: Dimension[];
public clauses?: Clause[];
public sort?: (Metric|Dimension)[];
}
......@@ -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;
}
......@@ -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 : [];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment