Skip to content

Commit 1c0a6d5

Browse files
author
Thomas Hansen
committed
Update readme and remove dependencies
1 parent 71f0a17 commit 1c0a6d5

7 files changed

+92
-106
lines changed

README.md

+74-88
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,109 @@
11
# Mongo-Postgres Query Converter
22
[MongoDB query documents](https://docs.mongodb.org/manual/tutorial/query-documents/) are quite powerful.
3-
This brings part of that usefulsness to PostgreSQL by letting you query in a similar way.
4-
This tool converts a Mongo query to a PostgreSQL "where" clause for data stored in a jsonb field.
5-
It also has additional converters for Mongo projections which are like "select" clauses and for update queries.
6-
7-
The goal of this is to eventually provide an adapter which lets Postgres serve as a drop in replacement for Mongo, but that is not there yet.
8-
Currently the project has many of the underlying conversions that will be required to do this.
9-
For that project, see [pgmongo](https://github.com/thomas4019/pgmongo).
10-
11-
### Select Query Example 1
12-
```javascript
13-
{ 'address.city': 'provo',
14-
name: 'Thomas',
15-
age: { '$gte': '30' } }
16-
```
17-
becomes the following Postgres query
18-
```sql
19-
(data->'address'->>'city'='provo') and (data->>'name'='Thomas') and (data->>'age'>='30')
20-
```
3+
This brings that usefulness to PostgreSQL by letting you query in a similar way.
4+
This tool converts a Mongo query to a PostgreSQL `where` clause for data stored in a jsonb field.
5+
It also has additional converters for Mongo projections which are like `select` clauses and for `update` queries.
216

22-
### Select Query Example 2
23-
```javascript
24-
{
25-
$or: [ { qty: { $gt: 100 } }, { price: { $lt: 9.95 } } ]
26-
}
27-
```
28-
becomes the following Postgres query
29-
```sql
30-
((data->'qty'>'100'::jsonb) OR (data->'price'<'9.95'::jsonb))
31-
```
7+
This tool is used by [pgmongo](https://github.com/thomas4019/pgmongo) which intends to provide a drop-in replacement for MongoDB.
328

33-
## Getting Started
9+
## Installation
3410

35-
```bash
11+
```sh
3612
npm install mongo-query-to-postgres-jsonb
3713
```
3814

39-
```javascript
40-
var mongoToPostgres = require('mongo-query-to-postgres-jsonb')
15+
## Simple Usage
16+
17+
```js
18+
var mToPsql = require('mongo-query-to-postgres-jsonb')
4119
var query = { field: 'value' }
42-
var sqlQuery = mongoToPostgres('data', query)
43-
console.log(sqlQuery)
20+
var sqlQuery = mToPsql('data', query)
4421
```
4522

46-
The first parameter, "data", is the name of your jsonb column in your postgres table.
47-
The second parameter is the Mongo style query.
48-
There is an optional third parameter explained in the next section.
23+
## API
4924

50-
### Projection Example
51-
52-
```javascript
53-
mongoToPostgres.convertSelect('data', { field: 1 })
54-
```
55-
becomes the following Postgres query
56-
```sql
57-
jsonb_build_object('field', data->'field', '_id', data->'_id')'
25+
```js
26+
var mToPsql = require('mongo-query-to-postgres-jsonb')
5827
```
5928

60-
### Update Example
29+
### mToPsql(sqlField, mongoQuery, [arrayFields])
6130

31+
#### sqlField
6232

63-
```javascript
64-
mongoToPostgres.convertUpdate('data', {
65-
$set: { active: true },
66-
$inc: { purchases: 2 }
67-
})
68-
```
69-
becomes the following Postgres query
70-
```sql
71-
jsonb_set(jsonb_set(data,'{active}','true'::jsonb),'{purchases}',to_jsonb(Cast(data->>'purchases' as numeric)+2))
72-
```
33+
This is the name of your jsonb column in your postgres table which holds all the data.
7334

74-
### Sort Example
35+
#### mongoQuery
7536

76-
```javascript
77-
mongoToPostgres.convertSort('data', {
78-
age: -1,
79-
'first.name': 1
80-
})
81-
```
82-
becomes the following Postgres query
83-
```sql
84-
data->'age' DESC, data->'first'->'name' ASC
85-
```
37+
An object containing MongoDB [query operators](https://docs.mongodb.com/manual/reference/operator/query/).
38+
39+
#### arrayFields
40+
41+
This tool doesn't know which fields are arrays so you can optionally specify a list of dotted paths which should be treated as an array.
42+
43+
### mToPsql.convertSelect(sqlField, projectionQuery)
44+
45+
#### projectionQuery
46+
47+
Object specifying which a subset of documents to return. Note: advanced [projection fields](https://docs.mongodb.com/manual/reference/operator/projection/) are not yet supported.
48+
49+
### mToPsql.convertUpdate(sqlField, updateQuery, [upsert])
50+
51+
#### updateQuery
52+
53+
Object containing [MongoDB operations](https://docs.mongodb.com/manual/reference/operator/update/) to apply to the documents.
54+
55+
#### upsert
56+
57+
Indicate that the query is being used for upserting. This will create a safer query that works if the original document doesn't already exist.
58+
59+
### mToPsql.convertSort(sqlField, sortQuery, [forceNumericSort])
60+
61+
#### sortQuery
62+
63+
Object containing [desired ordering](https://docs.mongodb.com/manual/reference/method/cursor.sort/#sort-asc-desc)
64+
65+
#### forceNumericSort
66+
67+
Cast strings to number when sorting.
68+
69+
## Examples
70+
71+
| Languages | MongoDB | Postgres |
72+
|------------|-------------------------------|---------------------------------------------------------------------------------|
73+
| Where | { 'address.city': 'provo' } | (data->'address'->>'city' = 'provo') |
74+
| Where | { $or: [ { qty: { $gt: 100 } }, { price: { $lt: 9.95 } } ] } | ((data->'qty'>'100'::jsonb) OR (data->'price'<'9.95'::jsonb)) |
75+
| Projection | { field: 1 } | jsonb_build_object('field', data->'field', '_id', data->'_id')' |
76+
| Update | { $set: { active: true } } | jsonb_set(data,'{active}','true'::jsonb) |
77+
| Update | { $inc: { purchases: 2 } } | jsonb_set(data,'{purchases}',to_jsonb(Cast(data->>'purchases' as numeric)+2)) |
78+
| Sort | { age: -1, 'first.name': 1} | data->'age' DESC, data->'first'->'name' ASC |
8679

87-
## Select: Match a Field Without Specifying Array Index
80+
## Advanced Select: Match a Field Without Specifying Array Index
8881

8982
* [Mongo Docs](https://docs.mongodb.org/manual/tutorial/query-documents/#match-a-field-without-specifying-array-index)
9083

91-
You can have a document with an array of objects that you want to match when any one of the elements in the array matches.
92-
This is implemented in SQL using a subquery so it may not be the most efficient.
84+
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.
85+
This tools implements it in SQL using a subquery so it is not be the most efficient.
9386

9487
Example document.
95-
```javascript
88+
```js
9689
{
9790
"courses": [{
98-
"distance": "5K"
99-
}, {
100-
"distance": "10K"
101-
}]
102-
}
91+
"distance": "5K"
92+
}, {
93+
"distance": "10K"
94+
}]
95+
]
10396
```
104-
Unlike Mongo, this tool doesn't know which fields are arrays and requires you to supply a list optionally as a third parameter.
105-
Example query to match when there is a course with a distance of "5K".
106-
```javascript
97+
Example query to match:
98+
```js
10799
mongoToPostgres('data', { 'courses.distance': '5K' }, ['courses'])
108100
```
109101
110102
## Supported Features
111103
* $eq, $gt, $gte, $lt, $lte, $ne
112-
* $or, $not, $nin
113-
* [$in](https://docs.mongodb.org/manual/reference/operator/query/in/#use-the-in-operator-to-match-values-in-an-array), $nin
104+
* $or, $not, [$in](https://docs.mongodb.org/manual/reference/operator/query/in/#use-the-in-operator-to-match-values-in-an-array), $nin
114105
* $elemMatch
115-
* [$regex](https://docs.mongodb.com/manual/reference/operator/query/regex/)
116-
* [$type](https://docs.mongodb.org/manual/reference/operator/query/type/#op._S_type)
117-
* [$size](https://docs.mongodb.org/manual/reference/operator/query/size/#op._S_size)
118-
* [$exists](https://docs.mongodb.org/manual/reference/operator/query/exists/#op._S_exists)
119-
* [$mod](https://docs.mongodb.com/manual/reference/operator/query/mod/)
120-
* [$all](https://docs.mongodb.com/manual/reference/operator/query/all/)
106+
* [$regex](https://docs.mongodb.com/manual/reference/operator/query/regex/), [$type](https://docs.mongodb.org/manual/reference/operator/query/type/#op._S_type), [$size](https://docs.mongodb.org/manual/reference/operator/query/size/#op._S_size), [$exists](https://docs.mongodb.org/manual/reference/operator/query/exists/#op._S_exists), [$mod](https://docs.mongodb.com/manual/reference/operator/query/mod/), [$all](https://docs.mongodb.com/manual/reference/operator/query/all/)
121107
122108
## Todo
123109
* Filtering
@@ -136,4 +122,4 @@ mongoToPostgres('data', { 'courses.distance': '5K' }, ['courses'])
136122
* [PostgreSQL json documentation](http://www.postgresql.org/docs/9.4/static/datatype-json.html)
137123
* [MongoDB query documention](https://docs.mongodb.org/manual/tutorial/query-documents/)
138124
* [PostgreSQL Array Functions](https://www.postgresql.org/docs/9.3/static/functions-array.html)
139-
* [JSON array to PostgreSQL Array](https://dba.stackexchange.com/questions/54283/how-to-turn-json-array-into-postgres-array/54289#54289)
125+
* [JSON array to PostgreSQL Array](https://dba.stackexchange.com/questions/54283/how-to-turn-json-array-into-postgres-array/54289#54289)

index.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const util = require('./util.js')
2-
const _ = require('lodash')
32

43
// These are the simple operators.
54
// Note that "is distinct from" needs to be used to ensure nulls are returned as expected, see https://modern-sql.com/feature/is-distinct-from
@@ -135,7 +134,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
135134
const type = util.getPostgresTypeName(value)
136135
return 'jsonb_typeof(' + text + ')=' + util.quote(type)
137136
case '$size':
138-
if (typeof value !== 'number' || value < 0 || !_.isInteger(value)) {
137+
if (typeof value !== 'number' || value < 0 || !Number.isInteger(value)) {
139138
throw new Error('$size only supports positive integer')
140139
}
141140
var text = util.pathToText(path, false)
@@ -212,4 +211,4 @@ module.exports.pathToText = util.pathToText
212211
module.exports.countUpdateSpecialKeys = util.countUpdateSpecialKeys
213212
module.exports.convertSelect = require('./select')
214213
module.exports.convertUpdate = require('./update')
215-
module.exports.convertSort = require('./sort')
214+
module.exports.convertSort = require('./sort')

package-lock.json

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

package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
22
"name": "mongo-query-to-postgres-jsonb",
3-
"version": "0.2.2",
3+
"version": "0.2.3",
44
"description": "Converts MongoDB queries to postgresql queries for jsonb fields.",
55
"main": "index.js",
66
"directories": {
77
"test": "test"
88
},
9-
"dependencies": {
10-
"lodash": "^4.17.11"
11-
},
9+
"dependencies": {},
1210
"devDependencies": {
1311
"chai": "^4.2.0",
1412
"mocha": "^5.2.0"

test/filter.js

+6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ describe('$size', function () {
151151
it('match array sizes', function() {
152152
assert.equal('jsonb_array_length(data->\'arr\')=3', convert('data', { arr: { $size: 3 } }))
153153
})
154+
it('fail for strings', function() {
155+
assert.throws(() => convert('data', { arr: { $size: 'abc' } }), '$size only supports positive integer')
156+
})
157+
it('fail for decimals', function() {
158+
assert.throws(() => convert('data', { arr: { $size: 3.5 } }), '$size only supports positive integer')
159+
})
154160
})
155161

156162
describe('$type', function () {

test/update.js

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ describe('update: ', function() {
2323
it('$set deep', function() {
2424
assert.equal(convertUpdate('data', { $set: { 'a.b': 2 } }), 'jsonb_set(jsonb_set(data,\'{a}\',COALESCE(data->\'a\', \'{}\'::jsonb)),\'{a,b}\',\'2\'::jsonb)')
2525
})
26+
it('$set fails for _id', function() {
27+
assert.throws(() => convertUpdate('data', { $set: { _id: 'b' } }), 'Mod on _id not allowed')
28+
})
2629

2730
it('$unset', function() {
2831
assert.equal(convertUpdate('data', { $unset: { field: 'value' } }), 'data #- \'{field}\'')

update.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const _ = require('lodash')
21
const util = require('./util.js')
32
const convertWhere = require('./index.js')
43

@@ -7,7 +6,7 @@ function convertOp(input, op, data, fieldName, upsert) {
76
const value = data[pathText]
87
delete data[pathText]
98
if (Object.keys(data).length > 0) {
10-
input = convertOp(input, op, data, fieldName, upsert)
9+
input = convertOp(input, op, Object.assign({}, data), fieldName, upsert)
1110
}
1211
const path = pathText.split('.')
1312
const pgPath = util.toPostgresPath(path)
@@ -26,7 +25,7 @@ function convertOp(input, op, data, fieldName, upsert) {
2625
}
2726
}
2827
}
29-
if (_.last(path) === '_id' && !upsert) {
28+
if (path[path.length - 1] === '_id' && !upsert) {
3029
throw new Error('Mod on _id not allowed')
3130
}
3231
return 'jsonb_set(' + input + ',' + pgPath + ',' + util.quote2(value) + ')'
@@ -78,13 +77,13 @@ var convert = function (fieldName, update, upsert) {
7877
let keys = Object.keys(update)
7978
// $set needs to happen first
8079
if (keys.includes('$set')) {
81-
keys = ['$set'].concat(_.pull(keys, '$set'))
80+
keys = ['$set'].concat(keys.filter((v) => v !== '$set'))
8281
}
8382
keys.forEach(function(key) {
8483
if (!util.updateSpecialKeys.includes(key)) {
8584
throw new Error('The <update> document must contain only update operator expressions.')
8685
}
87-
output = convertOp(output, key, _.cloneDeep(update[key]), fieldName, upsert)
86+
output = convertOp(output, key, Object.assign({}, update[key]), fieldName, upsert)
8887
})
8988
return output
9089
}

0 commit comments

Comments
 (0)