Skip to content

Commit 0d8bc98

Browse files
committed
Improve array matching
1 parent 51a7df9 commit 0d8bc98

8 files changed

+68
-15
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Cast strings to number when sorting.
8585
With MongoDB, you can search a document with a subarray of objects that you want to match when any one of the elements in the array matches.
8686
This tool implements it in SQL using a subquery, so it will likely not be the efficient on large datasets.
8787

88-
To enable subfield matching, you can pass a third parameter which is either an array of field names that will be assumed
88+
To enable subfield matching, you can pass a third parameter which is either an array of dotted paths that will be assumed
8989
to potentially be arrays or `true` if you want it to assume any field can be an array.
9090

9191
Example document:
@@ -110,6 +110,9 @@ This then creates a PostgreSQL query like the following:
110110
OR EXISTS (SELECT * FROM jsonb_array_elements(data->'courses')
111111
WHERE jsonb_typeof(data->'courses')='array' AND value->>'distance'='5K'))
112112
```
113+
114+
Note: nested paths are not yet supported, so passing ['courses', 'courses.distance'] won't support checking both.
115+
The first matching path is the one that will be used.
113116
114117
## Supported Features
115118
* $eq, $gt, $gte, $lt, $lte, $ne

index.js

+23-9
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,37 @@ var otherOps = {
1515
$all: true, $in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true, $type: true, $size: true, $exists: true, $mod: true, $text: true
1616
}
1717

18-
function shouldQueryAsArray(op, arrayPaths) {
18+
function getMatchingArrayPath(op, arrayPaths) {
1919
if (arrayPaths === true) {
2020
// always assume array path if true is passed
2121
return true
2222
}
2323
if (!arrayPaths || !Array.isArray(arrayPaths)) {
2424
return false
2525
}
26-
return arrayPaths.some(op => op.startsWith(arrayPaths))
26+
return arrayPaths.find(path => op.startsWith(path))
2727
}
2828

29-
function createElementOrArrayQuery(path, op, value, parent) {
30-
const subPath = op.split('.')
31-
const innerPath = subPath.length > 1 ? ['value', subPath.pop()] : ['value']
29+
/**
30+
* @param path array path current key
31+
* @param op current key, might be a dotted path
32+
* @param value
33+
* @param parent
34+
* @param arrayPathStr
35+
* @returns {string|string|*}
36+
*/
37+
function createElementOrArrayQuery(path, op, value, parent, arrayPathStr) {
38+
const arrayPath = arrayPathStr.split('.')
39+
const deeperPath = op.split('.').slice(arrayPath.length)
40+
const innerPath = ['value', ...deeperPath]
41+
const pathToMaybeArray = path.concat(arrayPath)
42+
43+
// TODO: nested array paths are not yet supported.
3244
const singleElementQuery = convertOp(path, op, value, parent, [])
33-
path = path.concat(subPath)
34-
const text = util.pathToText(path, false)
45+
46+
const text = util.pathToText(pathToMaybeArray, false)
3547
const safeArray = `jsonb_typeof(${text})='array' AND`
48+
3649
let arrayQuery = ''
3750
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
3851
if (typeof value['$size'] !== 'undefined') {
@@ -75,8 +88,9 @@ function createElementOrArrayQuery(path, op, value, parent) {
7588
* @param arrayPaths {Array} List of dotted paths that possibly need to be handled as arrays.
7689
*/
7790
function convertOp(path, op, value, parent, arrayPaths) {
78-
if (shouldQueryAsArray(op, arrayPaths)) {
79-
return createElementOrArrayQuery(path, op, value, parent)
91+
const arrayPath = getMatchingArrayPath(op, arrayPaths)
92+
if (arrayPath) {
93+
return createElementOrArrayQuery(path, op, value, parent, arrayPath)
8094
}
8195
switch(op) {
8296
case '$not':

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mongo-query-to-postgres-jsonb",
3-
"version": "0.2.7",
3+
"version": "0.2.8",
44
"description": "Converts MongoDB queries to postgresql queries for jsonb fields.",
55
"main": "index.js",
66
"directories": {

scripts/Dockerfile

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM mongo:3.4
2+
RUN apt-get update && apt-get -y install wget
3+
RUN wget -q https://github.com/mongodb/mongo/archive/r4.2.10.tar.gz
4+
RUN tar -xzf r4.2.10.tar.gz && mv mongo-r4.2.10 mongo
5+
RUN wget -q -O - https://deb.nodesource.com/setup_12.x | bash -
6+
RUN apt-get install -y nodejs
7+
WORKDIR /srv
8+
RUN npm i pgmongo
9+
RUN rm -r node_modules/mongo-query-to-postgres-jsonb
10+
11+
RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
12+
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
13+
RUN apt-get update
14+
RUN apt-get -y install postgresql
15+
16+
COPY . node_modules/mongo-query-to-postgres-jsonb
17+
COPY scripts/test.sh .

scripts/run_docker_tests.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
docker build --tag mongotest -f scripts/Dockerfile ./
2+
docker run --rm --name mongotest mongotest
3+
# docker exec -it mongotest /bin/bash

scripts/test.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pg_ctlcluster 13 main start
2+
su -c $'psql -c "ALTER USER postgres WITH PASSWORD \'postgres\';"' - postgres
3+
su -c $'psql -c "CREATE DATABASE test;"' - postgres
4+
DEBUG=pgmongo:* node node_modules/.bin/pgmongo localhost &
5+
mongo --port 27018 ../mongo/jstests/core/where1.js

test/filter.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,24 @@ describe('$mod', function () {
190190

191191
describe('Match a Field Without Specifying Array Index', function () {
192192
it('basic case', function() {
193-
assert.equal('(data @> \'{ "courses": { "distance": "5K" } }\' OR EXISTS (SELECT * FROM jsonb_array_elements(data->\'courses\') WHERE jsonb_typeof(data->\'courses\')=\'array\' AND value @> \'{ "distance": "5K" }\'))', convert('data', { 'courses.distance': '5K' }, ['courses']))
193+
assert.equal('(data @> \'{ "courses": { "distance": "5K" } }\' OR EXISTS (SELECT * FROM jsonb_array_elements' +
194+
'(data->\'courses\') WHERE jsonb_typeof(data->\'courses\')=\'array\' AND value @> \'{ "distance": "5K" }\'))',
195+
convert('data', { 'courses.distance': '5K' }, ['courses', 'other']))
196+
})
197+
it('basic deep case', function() {
198+
assert.equal('(data @> \'{ "courses": { "distance": "5K" } }\' OR EXISTS (SELECT * FROM jsonb_array_elements' +
199+
'(data->\'courses\'->\'distance\') WHERE jsonb_typeof(data->\'courses\'->\'distance\')=\'array\' AND value @> \'"5K"\'))',
200+
convert('data', { 'courses.distance': '5K' }, ['courses.distance']))
194201
})
195202
it('direct match', function() {
196-
assert.equal('(data @> \'{ "roles": "Admin" }\' OR EXISTS (SELECT * FROM jsonb_array_elements(data->\'roles\') WHERE jsonb_typeof(data->\'roles\')=\'array\' AND value @> \'"Admin"\'))', convert('data', { 'roles': 'Admin' }, ['roles']))
203+
assert.equal('(data @> \'{ "roles": "Admin" }\' OR EXISTS (SELECT * FROM jsonb_array_elements(' +
204+
'data->\'roles\') WHERE jsonb_typeof(data->\'roles\')=\'array\' AND value @> \'"Admin"\'))',
205+
convert('data', { 'roles': 'Admin' }, ['roles']))
197206
})
198207
it('$in', function() {
199-
assert.equal('(data->>\'roles\' IN (\'Test\', \'Admin\') OR EXISTS (SELECT * FROM jsonb_array_elements(data->\'roles\') WHERE jsonb_typeof(data->\'roles\')=\'array\' AND value #>>\'{}\' IN (\'Test\', \'Admin\')))', convert('data', { 'roles': { $in: ['Test', 'Admin'] } }, ['roles']))
208+
assert.equal('(data->>\'roles\' IN (\'Test\', \'Admin\') OR EXISTS (SELECT * FROM jsonb_array_elements(' +
209+
'data->\'roles\') WHERE jsonb_typeof(data->\'roles\')=\'array\' AND value #>>\'{}\' IN (\'Test\', \'Admin\')))',
210+
convert('data', { 'roles': { $in: ['Test', 'Admin'] } }, ['roles']))
200211
})
201212
})
202213
describe('special cases', function () {

0 commit comments

Comments
 (0)