Commit 24920706 authored by Lucas Fernandes de Oliveira's avatar Lucas Fernandes de Oliveira
Browse files

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 { ...@@ -199,7 +199,7 @@ export abstract class SQLAdapter extends Adapter {
materialized: false materialized: false
}); });
const from = "(" + const from = "(" +
this.buildQuery(partial, [partialJoin[i]]) + this.buildQuery(partial, [partialJoin[i]], false) +
") AS view_" + partial.id + "\n"; ") AS view_" + partial.id + "\n";
partialJoin[i].id = partial.id; partialJoin[i].id = partial.id;
...@@ -302,7 +302,7 @@ export abstract class SQLAdapter extends Adapter { ...@@ -302,7 +302,7 @@ export abstract class SQLAdapter extends Adapter {
materialized: false materialized: false
}); });
const viewFrom = "(" + const viewFrom = "(" +
this.buildQuery(partial, segment[i]) + this.buildQuery(partial, segment[i], false) +
") AS view_" + partial.id + "\n"; ") AS view_" + partial.id + "\n";
partialJoin.push({ partialJoin.push({
...@@ -507,7 +507,7 @@ export abstract class SQLAdapter extends Adapter { ...@@ -507,7 +507,7 @@ export abstract class SQLAdapter extends Adapter {
materialized: false materialized: false
}).id; }).id;
const viewFrom = "(" + const viewFrom = "(" +
this.buildQuery(partial, [partial0, partial1]) + this.buildQuery(partial, [partial0, partial1], false) +
") AS view_" + id + "\n"; ") AS view_" + id + "\n";
partialJoin.push({ partialJoin.push({
id: id, id: id,
...@@ -532,7 +532,7 @@ export abstract class SQLAdapter extends Adapter { ...@@ -532,7 +532,7 @@ export abstract class SQLAdapter extends Adapter {
layer to the query, that is in fact unnecessary. layer to the query, that is in fact unnecessary.
Think a way to remove-it. Think a way to remove-it.
*/ */
return this.buildQuery(view, partialJoin) + ";"; return this.buildQuery(view, partialJoin, true) + ";";
} }
private searchMaterializedViews(view: View): View[] { private searchMaterializedViews(view: View): View[] {
...@@ -551,10 +551,11 @@ export abstract class SQLAdapter extends Adapter { ...@@ -551,10 +551,11 @@ export abstract class SQLAdapter extends Adapter {
return r; return r;
} }
private buildQuery(target: View, views: ExpandedView[]) { private buildQuery(target: View, views: ExpandedView[], toSort: boolean) {
const metrics = target.metrics; const metrics = target.metrics;
const dimensions = target.dimensions; const dimensions = target.dimensions;
const clauses = target.clauses; const clauses = target.clauses;
const sort = target.sort;
let dimMap: {[key: string]: DimInfo} = {}; let dimMap: {[key: string]: DimInfo} = {};
let nameMap: {[key: string]: ExpandedView} = {}; let nameMap: {[key: string]: ExpandedView} = {};
...@@ -655,6 +656,11 @@ export abstract class SQLAdapter extends Adapter { ...@@ -655,6 +656,11 @@ export abstract class SQLAdapter extends Adapter {
} }
}); });
// Sorting
const order = sort.map((item) => {
return "\"" + item.name + "\"";
}).join(",");
// Assembly // Assembly
const projection = "SELECT " + elements.join(","); const projection = "SELECT " + elements.join(",");
...@@ -664,8 +670,9 @@ export abstract class SQLAdapter extends Adapter { ...@@ -664,8 +670,9 @@ export abstract class SQLAdapter extends Adapter {
if (grouped.length > 0) { if (grouped.length > 0) {
grouping = " GROUP BY " + grouped.join(","); 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 { ...@@ -29,6 +29,7 @@ interface StrQuery {
metrics: string; metrics: string;
dimensions: string; dimensions: string;
filters?: string; filters?: string;
sort?: string;
} }
function parseQuery(obj: Query): StrQuery { function parseQuery(obj: Query): StrQuery {
...@@ -80,6 +81,27 @@ describe("API data controller", () => { ...@@ -80,6 +81,27 @@ describe("API data controller", () => {
.end(done); .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) => { it("should respond 200 and get some data", (done) => {
request(server) request(server)
.get("/v1/data") .get("/v1/data")
...@@ -176,4 +198,31 @@ describe("API data controller", () => { ...@@ -176,4 +198,31 @@ describe("API data controller", () => {
.end(done); .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 { ...@@ -27,13 +27,17 @@ export class DataCtrl {
let metrics = req.query.metrics.split(",").filter((item: string) => item !== ""); let metrics = req.query.metrics.split(",").filter((item: string) => item !== "");
let dimensions = req.query.dimensions.split(",").filter((item: string) => item !== ""); let dimensions = req.query.dimensions.split(",").filter((item: string) => item !== "");
let clauses = []; let clauses = [];
let sort: string[] = [];
if (req.query.filters) { if (req.query.filters) {
clauses = req.query.filters.split(";").filter((item: string) => item !== ""); clauses = req.query.filters.split(";").filter((item: string) => item !== "");
} }
if (req.query.sort) {
sort = req.query.sort.split(",").filter((item: string) => item !== "");
}
let view; let view;
try { try {
let query: Query = { metrics: [], dimensions: [], clauses: [] }; let query: Query = { metrics: [], dimensions: [], clauses: [], sort: [] };
for (let i = 0; i < metrics.length; ++i) { for (let i = 0; i < metrics.length; ++i) {
query.metrics.push(req.engine.getMetricByName(metrics[i])); query.metrics.push(req.engine.getMetricByName(metrics[i]));
} }
...@@ -45,6 +49,26 @@ export class DataCtrl { ...@@ -45,6 +49,26 @@ export class DataCtrl {
for (let i = 0; i < clauses.length; ++i) { for (let i = 0; i < clauses.length; ++i) {
query.clauses.push(req.engine.parseClause(clauses[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); view = req.engine.query(query);
} }
catch (e) { catch (e) {
......
...@@ -26,4 +26,5 @@ export interface Query { ...@@ -26,4 +26,5 @@ export interface Query {
public metrics: Metric[]; public metrics: Metric[];
public dimensions: Dimension[]; public dimensions: Dimension[];
public clauses?: Clause[]; public clauses?: Clause[];
public sort?: (Metric|Dimension)[];
} }
...@@ -163,13 +163,16 @@ export class Engine { ...@@ -163,13 +163,16 @@ export class Engine {
const metrics = q.metrics; const metrics = q.metrics;
const dimensions = q.dimensions; const dimensions = q.dimensions;
const clauses = ((q.clauses) ? q.clauses : []); const clauses = ((q.clauses) ? q.clauses : []);
const sort = ((q.sort) ? q.sort : []);
if (optimalViews.length === 1 && if (optimalViews.length === 1 &&
optimalViews[0].metrics.length === metrics.length && optimalViews[0].metrics.length === metrics.length &&
optimalViews[0].dimensions.length === dimensions.length && optimalViews[0].dimensions.length === dimensions.length &&
optimalViews[0].clauses.length === clauses.length && optimalViews[0].clauses.length === clauses.length &&
optimalViews[0].sort.length === sort.length &&
optimalViews[0].metrics.every((item) => metrics.indexOf(item) !== -1) && optimalViews[0].metrics.every((item) => metrics.indexOf(item) !== -1) &&
optimalViews[0].dimensions.every((item) => dimensions.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]; return optimalViews[0];
} }
else { else {
...@@ -177,6 +180,7 @@ export class Engine { ...@@ -177,6 +180,7 @@ export class Engine {
metrics: metrics, metrics: metrics,
dimensions: dimensions, dimensions: dimensions,
clauses: clauses, clauses: clauses,
sort: sort,
materialized: false, materialized: false,
origin: false, // Never a dynamic generated view will be origin origin: false, // Never a dynamic generated view will be origin
childViews: optimalViews childViews: optimalViews
...@@ -196,8 +200,20 @@ export class Engine { ...@@ -196,8 +200,20 @@ export class Engine {
} }
function perfectMatch (array1: Clause[], function perfectMatch (array1: Clause[],
array2: Clause[]) { array2: Clause[]): boolean {
return array1.every((item: Clause) => { return array1.every((item: Clause) => {
return array2.some((otherItem: Clause) => item.id === otherItem.id); 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 { ...@@ -34,6 +34,7 @@ export interface ViewOptions {
keys?: Dimension[]; keys?: Dimension[];
origin: boolean; origin: boolean;
clauses?: Clause[]; clauses?: Clause[];
sort?: (Metric|Dimension)[];
materialized?: boolean; materialized?: boolean;
childViews?: View[]; childViews?: View[];
} }
...@@ -44,6 +45,7 @@ export class View { ...@@ -44,6 +45,7 @@ export class View {
public readonly dimensions: Dimension[]; public readonly dimensions: Dimension[];
public readonly keys: Dimension[]; public readonly keys: Dimension[];
public readonly clauses: Clause[]; public readonly clauses: Clause[];
public readonly sort: (Metric|Dimension)[];
public readonly materialized: boolean; public readonly materialized: boolean;
public readonly origin: boolean; public readonly origin: boolean;
public childViews: View[]; public childViews: View[];
...@@ -52,6 +54,7 @@ export class View { ...@@ -52,6 +54,7 @@ export class View {
this.metrics = options.metrics.sort(); this.metrics = options.metrics.sort();
this.dimensions = options.dimensions.sort(); this.dimensions = options.dimensions.sort();
this.clauses = (options.clauses) ? options.clauses.sort() : []; this.clauses = (options.clauses) ? options.clauses.sort() : [];
this.sort = (options.sort) ? options.sort : [];
this.materialized = options.materialized || false; this.materialized = options.materialized || false;
this.origin = options.origin || false; this.origin = options.origin || false;
this.childViews = (options.childViews) ? options.childViews : []; 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