Skip to content

Commit ccd8def

Browse files
committed
Improvements to containment usage
1 parent 12a971a commit ccd8def

File tree

6 files changed

+46
-27
lines changed

6 files changed

+46
-27
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ Cast strings to number when sorting.
7070

7171
| Languages | MongoDB | Postgres |
7272
|------------|-------------------------------|---------------------------------------------------------------------------------|
73-
| Where | { 'address.city': 'provo' } | (data->'address'->>'city' = 'provo') |
73+
| Where | { 'names.0': 'thomas' } | (data->'names'->>0 = 'thomas') |
74+
| Where | { 'address.city': 'provo' } | data @> { "address": '{ "city": "provo" }' } |
7475
| Where | { $or: [ { qty: { $gt: 100 } }, { price: { $lt: 9.95 } } ] } | ((data->'qty'>'100'::jsonb) OR (data->'price'<'9.95'::jsonb)) |
7576
| Projection | { field: 1 } | jsonb_build_object('field', data->'field', '_id', data->'_id')' |
7677
| Update | { $set: { active: true } } | jsonb_set(data,'{active}','true'::jsonb) |

index.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,19 @@ function convertOp(path, op, value, parent, arrayPaths) {
129129
case '$gte':
130130
case '$lt':
131131
case '$lte':
132-
var text = util.pathToText(path, typeof value == 'string')
133-
return text + ops[op] + util.quote(value)
134132
case '$ne':
135133
case '$eq':
136-
const [head, ...tail] = path
137-
return `${op=='$ne' ? 'NOT ' : ''}${head} @> ` + util.pathToObject([...tail, value])
134+
const isSimpleComparision = (op === '$eq' || op === '$ne')
135+
const pathContainsArrayAccess = path.some((key) => /^\d+$/.test(key))
136+
if (isSimpleComparision && !pathContainsArrayAccess) {
137+
// create containment query since these can use GIN indexes
138+
// See docs here, https://www.postgresql.org/docs/9.4/datatype-json.html#JSON-INDEXING
139+
const [head, ...tail] = path
140+
return `${op=='$ne' ? 'NOT ' : ''}${head} @> ` + util.pathToObject([...tail, value])
141+
} else {
142+
var text = util.pathToText(path, typeof value == 'string')
143+
return text + ops[op] + util.quote(value)
144+
}
138145
case '$type':
139146
var text = util.pathToText(path, false)
140147
const type = util.getPostgresTypeName(value)
@@ -175,8 +182,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
175182
*/
176183
var convert = function (path, query, arrayPaths, forceExact=false) {
177184
if (typeof query === 'string' || typeof query === 'boolean' || typeof query == 'number' || Array.isArray(query)) {
178-
var text = util.pathToText(path, typeof query == 'string')
179-
return text + '=' + util.quote(query)
185+
return convertOp(path, '$eq', query, {}, arrayPaths)
180186
}
181187
if (query === null) {
182188
var text = util.pathToText(path, false)

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.5",
3+
"version": "0.2.6",
44
"description": "Converts MongoDB queries to postgresql queries for jsonb fields.",
55
"main": "index.js",
66
"directories": {

test/filter.js

+22-17
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@ var convert = require('../index')
33

44
describe('string equality', function () {
55
it('should use ->>', function () {
6-
assert.equal('data->>\'name\'=\'thomas\'', convert('data', {name: 'thomas'}))
6+
assert.equal('data @> \'{ "name": "thomas" }\'', convert('data', {name: 'thomas'}))
77
})
88
it('should work with multiple', function () {
9-
assert.equal('(data->>\'a\'=\'a\' and data->>\'b\'=\'b\')', convert('data', {a: 'a', b: 'b'}))
9+
assert.equal('(data @> \'{ "a": "a" }\' and data @> \'{ "b": "b" }\')', convert('data', {a: 'a', b: 'b'}))
1010
})
1111
it('nesting does exact document matching', function() {
1212
assert.equal('data->\'test\'=\'{"cat":{"name":"oscar"}}\'::jsonb', convert('data', {test: {cat: {name: 'oscar'}} }))
1313
})
1414
it('should support nesting using the dot operator', function() {
15-
assert.equal('data->\'test\'->\'cat\'->>\'name\'=\'oscar\'', convert('data', {'test.cat.name': 'oscar'}))
15+
assert.equal('data @> \'{ "test": { "cat": { "name": "oscar" } } }\'', convert('data', {'test.cat.name': 'oscar'}))
1616
})
1717
})
1818

1919
describe('array equality', function () {
2020
it('should use =', function () {
21-
assert.equal('data->\'roles\'=\'["Admin"]\'::jsonb', convert('data', {'roles': ['Admin']}))
21+
assert.equal('data @> \'{ "roles": Admin }\'', convert('data', {'roles': ['Admin']}))
2222
})
2323
it('should matching numeric indexes', function() {
2424
assert.equal('data->\'roles\'->>0=\'Admin\'', convert('data', {'roles.0': 'Admin'}))
2525
})
2626
it('support element matching', function() {
27-
assert.equal('data->>\'roles\'=\'Admin\'', convert('data', {'roles': {$elemMatch: 'Admin'}}))
27+
assert.equal('data @> \'{ "roles": "Admin" }\'', convert('data', {'roles': {$elemMatch: 'Admin'}}))
2828
})
2929
})
3030

3131
describe('boolean equality', function () {
3232
it('should use ->', function () {
33-
assert.equal('data->\'hidden\'=\'false\'::jsonb', convert('data', {'hidden': false}))
33+
assert.equal('data @> \'{ "hidden": false }\'', convert('data', {'hidden': false}))
3434
})
3535
})
3636

3737
describe('number equality', function () {
3838
it('should use ->', function () {
39-
assert.equal('data->\'age\'=\'5\'::jsonb', convert('data', {'age': 5}))
39+
assert.equal('data @> \'{ "age": 5 }\'', convert('data', {'age': 5}))
4040
})
4141
})
4242

@@ -45,15 +45,15 @@ describe('$or', function () {
4545
assert.throws(() => convert('data', { $or: [] }), '$and/$or/$nor must be a nonempty array')
4646
})
4747
it('work with one parameter', function () {
48-
assert.equal('(data->>\'name\'=\'thomas\')', convert('data', {$or: [{name: 'thomas'}]}))
48+
assert.equal('(data @> \'{ "name": "thomas" }\')', convert('data', {$or: [{name: 'thomas'}]}))
4949
})
5050
it('work with two parameters', function () {
51-
assert.equal('(data->>\'name\'=\'thomas\' OR data->>\'name\'=\'hansen\')', convert('data', {$or: [{name: 'thomas'}, {name: 'hansen'}]}))
51+
assert.equal('(data @> \'{ "name": "thomas" }\' OR data @> \'{ "name": "hansen" }\')', convert('data', {$or: [{name: 'thomas'}, {name: 'hansen'}]}))
5252
})
5353
})
5454
describe('$nor', function () {
5555
it('work with two parameters', function () {
56-
assert.equal('((NOT data->>\'name\'=\'thomas\') AND (NOT data->>\'name\'=\'hansen\'))', convert('data', { $nor: [{ name: 'thomas' }, { name: 'hansen' }] }))
56+
assert.equal('((NOT data @> \'{ "name": "thomas" }\') AND (NOT data @> \'{ "name": "hansen" }\'))', convert('data', { $nor: [{ name: 'thomas' }, { name: 'hansen' }] }))
5757
})
5858
})
5959

@@ -62,13 +62,13 @@ describe('$and', function () {
6262
assert.throws(() => convert('data', { $and: [] }), '$and/$or/$nor must be a nonempty array')
6363
})
6464
it('work with one parameter', function () {
65-
assert.equal('(data->>\'name\'=\'thomas\')', convert('data', {$and: [{name: 'thomas'}]}))
65+
assert.equal('(data @> \'{ "name": "thomas" }\')', convert('data', {$and: [{name: 'thomas'}]}))
6666
})
6767
it('work with two parameters', function () {
68-
assert.equal('(data->>\'name\'=\'thomas\' AND data->>\'name\'=\'hansen\')', convert('data', {$and: [{name: 'thomas'}, {name: 'hansen'}]}))
68+
assert.equal('(data @> \'{ "name": "thomas" }\' AND data @> \'{ "name": "hansen" }\')', convert('data', {$and: [{name: 'thomas'}, {name: 'hansen'}]}))
6969
})
7070
it('should work implicitly', function () {
71-
assert.equal('(data->>\'type\'=\'food\' and data->\'price\'<\'9.95\'::jsonb)', convert('data', { type: 'food', price: { $lt: 9.95 } }))
71+
assert.equal('(data @> \'{ "type": "food" }\' and data->\'price\'<\'9.95\'::jsonb)', convert('data', { type: 'food', price: { $lt: 9.95 } }))
7272
})
7373
})
7474

@@ -102,6 +102,11 @@ describe('$not', function () {
102102
describe('comparision operators', function() {
103103
it('$eq', function () {
104104
assert.equal('data @> \'{ "type": "food" }\'', convert('data', { type: { $eq : 'food' } }))
105+
assert.equal('data @> \'{ "type": "food" }\'', convert('data', { type : 'food' }))
106+
assert.equal('data @> \'{ "address": { "city": "provo" } }\'', convert('data', { 'address.city': 'provo' }))
107+
})
108+
it('$eq inside array', function () {
109+
assert.equal('data->\'types\'->>0=\'food\'', convert('data', { 'types.0': { $eq : 'food' } }))
105110
})
106111
it('$ne', function () {
107112
assert.equal('NOT data @> \'{ "type": "food" }\'', convert('data', { type: { $ne : 'food' } }))
@@ -140,13 +145,13 @@ describe('regular expressions', function() {
140145

141146
describe('combined tests', function () {
142147
it('should handle ANDs and ORs together', function() {
143-
assert.equal('(data->>\'type\'=\'food\' and (data->\'qty\'>\'100\'::jsonb OR data->\'price\'<\'9.95\'::jsonb))', convert('data', {
148+
assert.equal('(data @> \'{ "type": "food" }\' and (data->\'qty\'>\'100\'::jsonb OR data->\'price\'<\'9.95\'::jsonb))', convert('data', {
144149
type: 'food',
145150
$or: [ { qty: { $gt: 100 } }, { price: { $lt: 9.95 } } ]
146151
}))
147152
})
148153
it('should add NOT and wrap in paratheses', function () {
149-
assert.equal('(data->>\'city\'=\'provo\' and data->\'pop\'>\'1000\'::jsonb)', convert('data', {city: 'provo', pop : { $gt : 1000 } }))
154+
assert.equal('(data @> \'{ "city": "provo" }\' and data->\'pop\'>\'1000\'::jsonb)', convert('data', {city: 'provo', pop : { $gt : 1000 } }))
150155
})
151156
})
152157

@@ -185,10 +190,10 @@ describe('$mod', function () {
185190

186191
describe('Match a Field Without Specifying Array Index', function () {
187192
it('basic case', function() {
188-
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(data->'courses') WHERE jsonb_typeof(data->'courses')='array' AND value @> '{ \"distance\": \"5K\" }'))", convert('data', { 'courses.distance': '5K' }, ['courses']))
189194
})
190195
it('direct match', function() {
191-
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']))
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']))
192197
})
193198
it('$in', function() {
194199
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']))

test/update.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('update: ', function() {
5656
assert.equal(convertUpdate('data', { $rename: { current: 'newN' } }), 'jsonb_set(data,\'{newN}\',data->\'current\') #- \'{current}\'')
5757
})
5858
it('$pull', function() {
59-
assert.equal(convertUpdate('data', { $pull: { cities: 'LA' } }), 'jsonb_set(data,\'{cities}\',to_jsonb(ARRAY(SELECT value FROM jsonb_array_elements(data->\'cities\') WHERE NOT value #>>\'{}\'=\'LA\')))')
59+
assert.equal(convertUpdate('data', { $pull: { cities: 'LA' } }), 'jsonb_set(data,\'{cities}\',to_jsonb(ARRAY(SELECT value FROM jsonb_array_elements(data->\'cities\') WHERE NOT value @> \'"LA"\')))')
6060
})
6161
it('$push', function() {
6262
assert.equal(convertUpdate('data', { $push: { cities: 'LA' } }), 'jsonb_set(data,\'{cities}\',to_jsonb(array_append(ARRAY(SELECT value FROM jsonb_array_elements(data->\'cities\')),\'"LA"\')))')

util.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ exports.pathToText = function(path, isString) {
3838
}
3939

4040
exports.pathToObject = function(path) {
41+
if (path.length === 1) {
42+
return exports.quote2(path[0])
43+
}
44+
return "'" + exports.pathToObjectHelper(path) + "'"
45+
}
46+
47+
exports.pathToObjectHelper = function(path) {
4148
if (path.length === 1) {
4249
if (typeof path[0] == 'string') {
4350
return `"${path[0]}"`
@@ -46,7 +53,7 @@ exports.pathToObject = function(path) {
4653
}
4754
}
4855
const [head, ...tail] = path
49-
return `'{ "${head}": ${exports.pathToObject(tail)} }'`
56+
return `{ "${head}": ${exports.pathToObjectHelper(tail)} }`
5057
}
5158

5259
exports.convertDotNotation = function(path, pathDotNotation) {

0 commit comments

Comments
 (0)