Skip to content

Commit

Permalink
fix(query-builder): support more operators in join conditions (#3399)
Browse files Browse the repository at this point in the history
  • Loading branch information
hehmonke committed Aug 25, 2022
1 parent 82d322c commit af885c8
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 26 deletions.
68 changes: 51 additions & 17 deletions packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,41 +428,75 @@ export class QueryBuilderHelper {
}

private appendJoinSubClause(clause: Knex.JoinClause, cond: Dictionary, key: string, operator?: '$and' | '$or'): void {
const m = operator === '$or' ? 'orOn' : 'andOn';
const c = operator === '$or' ? 'or' : 'and';
const m = `${c}On`;

if (cond[key] instanceof RegExp) {
if (this.isSimpleRegExp(cond[key])) {
return void clause[m](this.mapper(key), 'like', this.knex.raw('?', this.getRegExpParam(cond[key])));
}

if (Utils.isPlainObject(cond[key])) {
return this.processObjectSubClause(cond, key, clause, m);
if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) {
return this.processObjectSubClause(cond, key, clause, c);
}

if (QueryBuilderHelper.isCustomExpression(key)) {
return this.processCustomExpression(clause, m, key, cond);
}

const op = cond[key] === null ? 'is' : '=';
clause[m](this.knex.raw(`${this.knex.ref(this.mapper(key, QueryType.SELECT, cond[key]))} ${op} ?`, cond[key]));
if (cond[key] === null) {
clause[`${c}OnNull`](this.mapper(key));
} else {
clause[m](this.knex.raw(`${this.knex.ref(this.mapper(key, QueryType.SELECT, cond[key]))} = ?`, cond[key]));
}
}

private processObjectSubClause(cond: any, key: string, clause: Knex.JoinClause, m: 'andOn' | 'orOn'): void {
private processObjectSubClause(cond: any, key: string, clause: Knex.JoinClause, c: 'and' | 'or'): void {
// grouped condition for one field
if (Utils.getObjectKeysSize(cond[key]) > 1) {
const subCondition = Object.entries(cond[key]).map(([subKey, subValue]) => ({ [key]: { [subKey]: subValue } }));
return void clause[m](inner => subCondition.map(sub => this.appendJoinClause(inner, sub, '$and')));
let value = cond[key];

if (Utils.getObjectKeysSize(value) > 1) {
const subCondition = Object.entries(value).map(([subKey, subValue]) => ({ [key]: { [subKey]: subValue } }));
return void clause[`${c}On`](inner => subCondition.map(sub => this.appendJoinClause(inner, sub, '$and')));
}

// operators
for (const [op, replacement] of Object.entries(QueryOperator)) {
if (!(op in cond[key])) {
continue;
}
if (value instanceof RegExp) {
value = { $re: value.source };
}

const op = Object.keys(QueryOperator).find(op => op in value);

if (!op) {
return;
}

if (['$eq', '$ne'].includes(op) && value[op] === null) {
return void clause[`${c}${op === '$eq' ? 'OnNull' : 'OnNotNull'}`](this.mapper(key));
}

clause[m](this.mapper(key), replacement, this.knex.raw('?', cond[key][op]));
if (op === '$exists') {
return void clause[`${c}${value[op] ? 'OnNotNull' : 'OnNull'}`](this.mapper(key));
}

if (op === '$in') {
return void clause[`${c}OnIn`](this.mapper(key), value[op]);
}

break;
if (op === '$nin') {
return void clause[`${c}OnNotIn`](this.mapper(key), value[op]);
}

if (op === '$fulltext') {
const [fromAlias, fromField] = this.splitField(key);
const property = this.getProperty(fromField, fromAlias);

return void clause[`${c}On`](this.knex.raw(this.platform.getFullTextWhereClause(property!), {
column: this.mapper(key),
query: value[op],
}));
}

const replacement = this.getOperatorReplacement(op, value);
clause[`${c}On`](this.mapper(key), replacement, this.knex.raw('?', value[op]));
}

getQueryOrder(type: QueryType, orderBy: FlatQueryOrderMap | FlatQueryOrderMap[], populate: Dictionary<string>): string {
Expand Down
37 changes: 28 additions & 9 deletions tests/QueryBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,22 +367,41 @@ describe('QueryBuilder', () => {
.leftJoin('a.books', 'b', {
'b.foo:gte': '123',
'b.baz': { $gt: 1, $lte: 10 },
'b.title': { $fulltext: 'test' },
'b.qux': {},
'$or': [
{ 'b.foo': null, 'b.baz': 0, 'b.bar:ne': 1 },
{ 'b.bar': /321.*/ },
{ $and: [
{ 'json_contains(`a`.`meta`, ?)': [{ 'b.foo': 'bar' }] },
{ 'json_contains(`a`.`meta`, ?) = ?': [{ 'b.foo': 'bar' }, false] },
{ 'lower(b.bar)': '321' },
] },
{
'b.foo': null,
'b.qux': { $ne: null },
'b.quux': { $eq: null },
'b.baz': 0,
'b.bar:ne': 1,
},
{
'b.foo': { $nin: [0,1] },
'b.baz': { $in: [2,3] },
'b.qux': { $exists: true },
'b.bar': /test/,
},
{
'b.qux': { $exists: false },
'b.bar': /^(te){1,3}st$/,
},
{
$and: [
{ 'json_contains(`a`.`meta`, ?)': [{ 'b.foo': 'bar' }] },
{ 'json_contains(`a`.`meta`, ?) = ?': [{ 'b.foo': 'bar' }, false] },
{ 'lower(b.bar)': '321' },
],
},
],
})
.where({ 'b.title': 'test 123' });
const sql = 'select `a`.*, `b`.* from `author2` as `a` ' +
'left join `book2` as `b` on `a`.`id` = `b`.`author_id` and `b`.`foo` >= ? and (`b`.`baz` > ? and `b`.`baz` <= ?) and ((`b`.`foo` is ? and `b`.`baz` = ? and `b`.`bar` != ?) or `b`.`bar` like ? or (json_contains(`a`.`meta`, ?) and json_contains(`a`.`meta`, ?) = ? and lower(b.bar) = ?)) ' +
'left join `book2` as `b` on `a`.`id` = `b`.`author_id` and `b`.`foo` >= ? and (`b`.`baz` > ? and `b`.`baz` <= ?) and match(`b`.`title`) against (? in boolean mode) and ((`b`.`foo` is null and `b`.`qux` is not null and `b`.`quux` is null and `b`.`baz` = ? and `b`.`bar` != ?) or (`b`.`foo` not in (?, ?) and `b`.`baz` in (?, ?) and `b`.`qux` is not null and `b`.`bar` like ?) or (`b`.`qux` is null and `b`.`bar` regexp ?) or (json_contains(`a`.`meta`, ?) and json_contains(`a`.`meta`, ?) = ? and lower(b.bar) = ?)) ' +
'where `b`.`title` = ?';
expect(qb.getQuery()).toEqual(sql);
expect(qb.getParams()).toEqual(['123', 1, 10, null, 0, 1, '%321%%', '{"b.foo":"bar"}', '{"b.foo":"bar"}', false, '321', 'test 123']);
expect(qb.getParams()).toEqual(['123', 1, 10, 'test', 0, 1, 0, 1, 2, 3, '%test%', '^(te){1,3}st$', '{"b.foo":"bar"}', '{"b.foo":"bar"}', false, '321', 'test 123']);
});

test('select leftJoin m:n owner', async () => {
Expand Down

0 comments on commit af885c8

Please sign in to comment.