Skip to content

Commit 65cfaf8

Browse files
author
Thomas Hansen
committed
Improve support for querying arrays including $all and $elemMatch.
1 parent f5eeb09 commit 65cfaf8

File tree

2 files changed

+49
-15
lines changed

2 files changed

+49
-15
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,16 @@ mongoToPostgres('data', { 'courses.distance': '5K' }, ['courses'])
120120

121121
## Todo
122122
* Filtering
123-
* [Match an array element](https://docs.mongodb.org/manual/tutorial/query-documents/#match-an-array-element)
124123
* [$all](https://docs.mongodb.com/manual/reference/operator/query/all/)
125124
* [$expr](https://docs.mongodb.com/manual/reference/operator/query/expr/)
126125
* [Bitwise Operators](https://docs.mongodb.com/manual/reference/operator/query-bitwise/)
127126
* Update
128127
* [$pop](https://docs.mongodb.com/manual/reference/operator/update/pop/)
129128
* [$currentDate](https://docs.mongodb.com/manual/reference/operator/update/currentDate/)
130129
* [$setOnInsert](https://docs.mongodb.com/manual/reference/operator/update/setOnInsert/)
131-
* Other
132-
* [Sort query conversions](https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort)
130+
131+
## Cannot Support
132+
* [$where](https://docs.mongodb.com/manual/reference/operator/query/where/)
133133

134134
## See also
135135
* [PostgreSQL json/jsonb functions and operators](http://www.postgresql.org/docs/9.4/static/functions-json.html)

index.js

+46-12
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,48 @@ var ops = {
1111
}
1212

1313
var otherOps = {
14-
$in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true, $type: true, $size: true, $exists: true, $mod: true
14+
$all: true, $in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true, $type: true, $size: true, $exists: true, $mod: true
1515
}
1616

1717
function convertOp(path, op, value, parent, arrayPaths) {
1818
if (arrayPaths) {
1919
for (var arrPath of arrayPaths) {
2020
if (op.startsWith(arrPath)) {
21-
var subPath = op.split('.')
22-
var innerPath = subPath.length > 1 ? ['value', subPath.pop()] : ['value']
23-
var innerText = util.pathToText(innerPath, typeof value === 'string')
21+
const subPath = op.split('.')
22+
const innerPath = subPath.length > 1 ? ['value', subPath.pop()] : ['value']
23+
const singleElementQuery = convertOp(path, op, value, parent, [])
2424
path = path.concat(subPath)
25-
var text = util.pathToText(path, false)
26-
if (value['$in']) {
27-
const sub = convert(innerPath, value)
28-
return 'EXISTS (SELECT * FROM jsonb_array_elements(' + text + ') WHERE ' + sub + ')'
25+
const text = util.pathToText(path, false)
26+
const safeArray = "jsonb_typeof(data->'a')='array' AND";
27+
let arrayQuery = '';
28+
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
29+
if (value['$elemMatch']) {
30+
const sub = convert(innerPath, value['$elemMatch'], [], false)
31+
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
32+
} else if (value['$in']) {
33+
const sub = convert(innerPath, value, [], true)
34+
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
35+
} else if (value['$all']) {
36+
const cleanedValue = value['$all'].filter((v) => (v !== null && typeof v !== 'undefined'))
37+
arrayQuery = '(' + cleanedValue.map(function (subquery) {
38+
const sub = convert(innerPath, subquery, [], false)
39+
return `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
40+
}).join(' AND ') + ')'
41+
} else {
42+
const params = value
43+
arrayQuery = '(' + Object.keys(params).map(function (subKey) {
44+
const sub = convert(innerPath, { [subKey]: params[subKey] }, [], true)
45+
return `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
46+
}).join(' AND ') + ')'
47+
}
2948
} else {
30-
return 'EXISTS (SELECT * FROM jsonb_array_elements(' + text + ') WHERE ' + innerText + '=' + util.quote(value) + ')'
49+
const sub = convert(innerPath, value, [], true)
50+
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
51+
}
52+
if (!arrayQuery) {
53+
return singleElementQuery
3154
}
55+
return `(${singleElementQuery} OR ${arrayQuery})`
3256
}
3357
}
3458
}
@@ -53,8 +77,10 @@ function convertOp(path, op, value, parent, arrayPaths) {
5377
}
5478
return '(' + value.map((subquery) => convert(path, subquery)).join(op === '$or' ? ' OR ' : ' AND ') + ')'
5579
}
80+
// TODO (make sure this handles multiple elements correctly)
5681
case '$elemMatch':
57-
return util.pathToText(path, false) + ' @> \'' + util.stringEscape(JSON.stringify(value)) + '\'::jsonb'
82+
return convert(path, value, arrayPaths)
83+
//return util.pathToText(path, false) + ' @> \'' + util.stringEscape(JSON.stringify(value)) + '\'::jsonb'
5884
case '$in':
5985
case '$nin':
6086
if (value.length === 1) {
@@ -105,7 +131,15 @@ function convertOp(path, op, value, parent, arrayPaths) {
105131
}
106132
}
107133

108-
var convert = function (path, query, arrayPaths) {
134+
/**
135+
* Convert a filter expression to the corresponding PostgreSQL text.
136+
* @param path {Array} The current path
137+
* @param query {Mixed} Any value
138+
* @param arrayPaths {Array} List of dotted paths that possibly need to be handled as arrays.
139+
* @param forceExact {Boolean} When true, an exact match will be required.
140+
* @returns The corresponding PSQL expression
141+
*/
142+
var convert = function (path, query, arrayPaths, forceExact=false) {
109143
if (typeof query === 'string' || typeof query === 'boolean' || typeof query == 'number' || Array.isArray(query)) {
110144
var text = util.pathToText(path, typeof query == 'string')
111145
return text + '=' + util.quote(query)
@@ -124,7 +158,7 @@ var convert = function (path, query, arrayPaths) {
124158
return 'TRUE'
125159
}
126160
var specialKeys = Object.keys(query).filter(function (key) {
127-
return (path.length === 1) || key in ops || key in otherOps
161+
return (path.length === 1 && !forceExact) || key in ops || key in otherOps
128162
})
129163
switch (specialKeys.length) {
130164
case 0:

0 commit comments

Comments
 (0)