Skip to content

Optimize redundant logic used in queries #7061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 16, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### master
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...master)
- IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz)

### 4.4.0
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.3.0...4.4.0)
Expand Down
45 changes: 45 additions & 0 deletions spec/DatabaseController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,51 @@ describe('DatabaseController', function () {
done();
});
});

describe('reduceOperations', function () {
const databaseController = new DatabaseController();

it('objectToEntriesStrings', done => {
const output = databaseController.objectToEntriesStrings({ a: 1, b: 2, c: 3 });
expect(output).toEqual(['"a":1', '"b":2', '"c":3']);
done();
});

it('reduceOrOperation', done => {
expect(databaseController.reduceOrOperation({ a: 1 })).toEqual({ a: 1 });
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { b: 2 }] })).toEqual({
$or: [{ a: 1 }, { b: 2 }],
});
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 2 }] })).toEqual({
$or: [{ a: 1 }, { a: 2 }],
});
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 1 }] })).toEqual({ a: 1 });
expect(
databaseController.reduceOrOperation({ $or: [{ a: 1, b: 2, c: 3 }, { a: 1 }] })
).toEqual({ a: 1 });
expect(
databaseController.reduceOrOperation({ $or: [{ b: 2 }, { a: 1, b: 2, c: 3 }] })
).toEqual({ b: 2 });
done();
});

it('reduceAndOperation', done => {
expect(databaseController.reduceAndOperation({ a: 1 })).toEqual({ a: 1 });
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { b: 2 }] })).toEqual({
$and: [{ a: 1 }, { b: 2 }],
});
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 2 }] })).toEqual({
$and: [{ a: 1 }, { a: 2 }],
});
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 1 }] })).toEqual({
a: 1,
});
expect(
databaseController.reduceAndOperation({ $and: [{ a: 1, b: 2, c: 3 }, { b: 2 }] })
).toEqual({ a: 1, b: 2, c: 3 });
done();
});
});
});

function buildCLP(pointerNames) {
Expand Down
67 changes: 65 additions & 2 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,69 @@ class DatabaseController {
});
}

// This helps to create intermediate objects for simpler comparison of
// key value pairs used in query objects. Each key value pair will represented
// in a similar way to json
objectToEntriesStrings(query: any): Array<string> {
return Object.entries(query).map(a => a.map(s => JSON.stringify(s)).join(':'));
}

// Naive logic reducer for OR operations meant to be used only for pointer permissions.
reduceOrOperation(query: { $or: Array<any> }): any {
if (!query.$or) {
return query;
}
const queries = query.$or.map(q => this.objectToEntriesStrings(q));
for (let i = 0; i < queries.length - 1; i++) {
for (let j = i + 1; j < queries.length; j++) {
const [shorter, longer] = queries[i].length > queries[j].length ? [j, i] : [i, j];
const foundEntries = queries[shorter].reduce(
(acc, entry) => acc + (queries[longer].includes(entry) ? 1 : 0),
0
);
if (foundEntries === queries[shorter].length) {
// If the shorter query is completely contained in the longer one, we can strike
// out the longer query.
query.$or.splice(longer, 1);
queries.splice(longer, 1);
}
}
}
if (query.$or.length === 1) {
query = { ...query, ...query.$or[0] };
delete query.$or;
}
return query;
}

// Naive logic reducer for AND operations meant to be used only for pointer permissions.
reduceAndOperation(query: { $and: Array<any> }): any {
if (!query.$and) {
return query;
}
const queries = query.$and.map(q => this.objectToEntriesStrings(q));
for (let i = 0; i < queries.length - 1; i++) {
for (let j = i + 1; j < queries.length; j++) {
const [shorter, longer] = queries[i].length > queries[j].length ? [j, i] : [i, j];
const foundEntries = queries[shorter].reduce(
(acc, entry) => acc + (queries[longer].includes(entry) ? 1 : 0),
0
);
if (foundEntries === queries[shorter].length) {
// If the shorter query is completely contained in the longer one, we can strike
// out the shorter query.
query.$and.splice(shorter, 1);
queries.splice(shorter, 1);
}
}
}
if (query.$and.length === 1) {
query = { ...query, ...query.$and[0] };
delete query.$and;
}
return query;
}

// Constraints query using CLP's pointer permissions (PP) if any.
// 1. Etract the user id from caller's ACLgroup;
// 2. Exctract a list of field names that are PP for target collection and operation;
Expand Down Expand Up @@ -1448,13 +1511,13 @@ class DatabaseController {
}
// if we already have a constraint on the key, use the $and
if (Object.prototype.hasOwnProperty.call(query, key)) {
return { $and: [queryClause, query] };
return this.reduceAndOperation({ $and: [queryClause, query] });
}
// otherwise just add the constaint
return Object.assign({}, query, queryClause);
});

return queries.length === 1 ? queries[0] : { $or: queries };
return queries.length === 1 ? queries[0] : this.reduceOrOperation({ $or: queries });
} else {
return query;
}
Expand Down