mongery 0.0.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +33 -4
- data/lib/mongery.rb +146 -62
- data/lib/mongery/schema.rb +32 -0
- data/lib/mongery/version.rb +1 -1
- data/mongery.gemspec +2 -1
- data/spec/mongery/builder_schema_spec.rb +55 -0
- data/spec/mongery/builder_spec.rb +49 -23
- metadata +21 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f07b3b2676098909d2b127f75970b95b4e4be027
|
4
|
+
data.tar.gz: 9d5c86b70c843e233ebeb5d101c44e6f9ddef6ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0442d0f7cf0f20fbf742fb5ece86d0bbed8f68642141742bcf2ef071c5bc78880d3bdef67e80a3f9b3eda7986695cdff66abe3baa308387746fb2cbb12f7bc35
|
7
|
+
data.tar.gz: 258eb17e770f85b4ae08e1700429b77c9a6867dfd3137b002eba77caaeb774e6815bce2b3cfdde6307403f960c6dd70cdf7db03dbf912143b7e6029c07f2e88c
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## 1.0.0 (2014/12/9)
|
2
|
+
- This is a MAJOR update and is incompatible to the previous version
|
3
|
+
- All JSON fields are now represented with JSON path (`data#>>{path}`)
|
4
|
+
- $in query is now translated to SQL IN
|
5
|
+
- More accurate comparison with nil or numeric values
|
6
|
+
- operators such as $lt, $gt can be used at the same time to chain them with AND
|
7
|
+
- Supports partial JSON value match when you don't use $- operators
|
8
|
+
- Supports casting JSON value based on JSON Schema rather than based on the bound values.
|
9
|
+
|
1
10
|
## 0.0.5 (2014/11/10)
|
2
11
|
- Use JSON path expression for nested data to avoid errors like "cannot extract element from a scalar"
|
3
12
|
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ This could be useful in situations where you migrate off of MongoDB backend to P
|
|
6
6
|
|
7
7
|
## Limitation
|
8
8
|
|
9
|
-
Currently, Mongery supports the limited set of queries to use with PostgreSQL 9.3. Most query on JSON data will end up with the full table scan
|
9
|
+
Currently, Mongery supports the limited set of queries to use with PostgreSQL 9.3. Most query on JSON data will end up with the full table scan, unless you manually create expression index for the JSON path you want to query on.
|
10
10
|
|
11
11
|
See the spec file in `spec` directory for the supported conversion.
|
12
12
|
|
@@ -31,18 +31,47 @@ builder.find({ _id: 'abcd' }).limit(1).to_sql
|
|
31
31
|
# => SELECT data FROM objects WHERE id = 'abcd' LIMIT 1
|
32
32
|
|
33
33
|
builder.find({ age: {"$gte" => 21 } }).sort({ name: -1 }).to_sql
|
34
|
-
# => SELECT data FROM objects WHERE (data
|
34
|
+
# => SELECT data FROM objects WHERE (data#>>'{age}')::integer >= 21 ORDER BY data#>>'{name}' DESC
|
35
|
+
|
36
|
+
builder.find({ "address.city": {"$in" => ["San Francisco", "Tokyo"] } })
|
37
|
+
# => SELECT data FROM objects WHERE data#>>'{address,city}' IN ("San Francisco", "Tokyo")
|
35
38
|
|
36
39
|
builder.insert({ _id: 'foobar' }).to_sql
|
37
40
|
# => INSERT INTO "objects" ("id", "data") VALUES ('foobar', '{"_id":"foobar"}')
|
38
41
|
|
39
42
|
builder.find({ age: {"$gte" => 21 } }).update({ name: "John" }).to_sql
|
40
|
-
# => UPDATE "objects" SET "data" = '{"name":"John"}' WHERE (data
|
43
|
+
# => UPDATE "objects" SET "data" = '{"name":"John"}' WHERE (data#>>'{age}')::integer >= 21
|
41
44
|
|
42
45
|
builder.find({ age: {"$eq" => 55 } }).delete.to_sql
|
43
|
-
# => DELETE FROM "objects" WHERE (data
|
46
|
+
# => DELETE FROM "objects" WHERE (data#>>'{age}')::integer = 21
|
47
|
+
```
|
48
|
+
|
49
|
+
### Type Casting
|
50
|
+
|
51
|
+
By default, types are dynamically casted depending on the query values.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
builder.find({ age: {"$eq" => 55 } }).to_sql
|
55
|
+
# => SELECT FROM "objects" WHERE (data#>>'{age}')::integer = 21
|
44
56
|
```
|
45
57
|
|
58
|
+
Mongery has an **experimental support** for JSON Schema. You can pass JsonSchema::Schema object created with [json_schema gem](https://github.com/brandur/json_schema) to the Mongery::Builder constructor, to make it cast the query type based on the schema rather than the bound value.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
schema_data = {
|
62
|
+
"type" => "object",
|
63
|
+
"properties" => {
|
64
|
+
"name" => {
|
65
|
+
"type" => "string"
|
66
|
+
},
|
67
|
+
},
|
68
|
+
}
|
69
|
+
schema = JsonSchema.parse!(schema_data)
|
70
|
+
|
71
|
+
builder = Mongery::Builder.new(:objects, ActiveRecord::Base, schema)
|
72
|
+
```
|
73
|
+
|
74
|
+
|
46
75
|
## See Also
|
47
76
|
|
48
77
|
* [mosql](https://github.com/stripe/mosql)
|
data/lib/mongery.rb
CHANGED
@@ -1,29 +1,32 @@
|
|
1
1
|
require "mongery/version"
|
2
|
+
require "mongery/schema"
|
2
3
|
require "arel"
|
3
4
|
|
4
5
|
module Mongery
|
5
6
|
class Builder
|
6
|
-
attr_reader :model, :table
|
7
|
+
attr_reader :model, :table, :schema
|
7
8
|
|
8
|
-
def initialize(model, engine = ActiveRecord::Base)
|
9
|
+
def initialize(model, engine = ActiveRecord::Base, schema = nil)
|
9
10
|
@model = model
|
10
11
|
@table = Arel::Table.new(model, engine)
|
12
|
+
@schema = Schema.new(schema) if schema
|
11
13
|
end
|
12
14
|
|
13
15
|
def find(*args)
|
14
|
-
Query.new(table).where(*args)
|
16
|
+
Query.new(table, schema).where(*args)
|
15
17
|
end
|
16
18
|
|
17
19
|
def insert(*args)
|
18
|
-
Query.new(table).insert(*args)
|
20
|
+
Query.new(table, schema).insert(*args)
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
24
|
class Query
|
23
|
-
attr_reader :table
|
25
|
+
attr_reader :table, :schema
|
24
26
|
|
25
|
-
def initialize(table)
|
27
|
+
def initialize(table, schema)
|
26
28
|
@table = table
|
29
|
+
@schema = schema
|
27
30
|
@condition = nil
|
28
31
|
end
|
29
32
|
|
@@ -102,102 +105,183 @@ module Mongery
|
|
102
105
|
end
|
103
106
|
|
104
107
|
def translate(query)
|
105
|
-
chain(:and, query.map {
|
108
|
+
chain(:and, query.map {|col, value| translate_cv(col, value) })
|
106
109
|
end
|
107
110
|
|
108
111
|
def translate_cv(col, value)
|
109
112
|
case col.to_s
|
110
|
-
when "_id"
|
111
|
-
translate_value(table[:id], value)
|
112
113
|
when "$or"
|
113
114
|
chain(:or, value.map {|q| translate(q) })
|
114
115
|
when "$and"
|
115
|
-
chain(:and, value.map {
|
116
|
+
chain(:and, value.map {|q| translate(q) })
|
116
117
|
when /^\$/
|
117
118
|
raise UnsupportedQuery, "Unsupported operator #{col}"
|
119
|
+
when "_id"
|
120
|
+
translate_value(table[:id], value)
|
118
121
|
else
|
119
|
-
|
122
|
+
if schema
|
123
|
+
translate_value_schema(col, sql_json_path(col), value)
|
124
|
+
else
|
125
|
+
translate_value_dynamic(sql_json_path(col), value)
|
126
|
+
end
|
120
127
|
end
|
121
128
|
end
|
122
129
|
|
123
|
-
|
130
|
+
OPERATOR_MAP = {
|
131
|
+
"$in" => :in, "$eq" => :eq, "$ne" => :not_eq,
|
132
|
+
"$gt" => :gt, "$gte" => :gteq, "$lt" => :lt, "$lte" => :lteq,
|
133
|
+
}
|
134
|
+
|
135
|
+
def translate_value(col, value)
|
124
136
|
case value
|
125
137
|
when Hash
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
col.send(COMPARE_MAPS[ops.first], val)
|
137
|
-
when "$eq"
|
138
|
-
col.eq(val)
|
139
|
-
when "$ne"
|
140
|
-
col.not_eq(val)
|
141
|
-
when /^\$/
|
142
|
-
raise UnsupportedQuery, "Unknown operator #{ops.first}"
|
138
|
+
if has_operator?(value)
|
139
|
+
chain(:and, value.map {|op, val|
|
140
|
+
if OPERATOR_MAP.key?(op)
|
141
|
+
col.send(OPERATOR_MAP[op], val)
|
142
|
+
else
|
143
|
+
raise UnsupportedQuery, "Unknown operator #{op}"
|
144
|
+
end
|
145
|
+
})
|
146
|
+
else
|
147
|
+
col.eq(value.to_json)
|
143
148
|
end
|
144
149
|
else
|
145
150
|
col.eq(value)
|
146
151
|
end
|
147
152
|
end
|
148
153
|
|
149
|
-
|
154
|
+
def translate_value_dynamic(col, value)
|
155
|
+
case value
|
156
|
+
when String, TrueClass, FalseClass
|
157
|
+
compare(col, value.to_s, :eq)
|
158
|
+
when Numeric, NilClass
|
159
|
+
compare(col, value, :eq)
|
160
|
+
when Hash
|
161
|
+
if has_operator?(value)
|
162
|
+
chain(:and, value.map {|op, val|
|
163
|
+
case op
|
164
|
+
when "$in"
|
165
|
+
if val.all? {|v| v.is_a? Numeric }
|
166
|
+
wrap(col, val.first).in(val)
|
167
|
+
else
|
168
|
+
col.in(val.map(&:to_s))
|
169
|
+
end
|
170
|
+
when "$eq", "$ne", "$gt", "$gte", "$lt", "$lte"
|
171
|
+
compare(col, val, OPERATOR_MAP[op])
|
172
|
+
else
|
173
|
+
raise UnsupportedQuery, "Unknown operator #{op}"
|
174
|
+
end
|
175
|
+
})
|
176
|
+
else
|
177
|
+
col.eq(value.to_json)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
col.eq(value.to_json)
|
181
|
+
end
|
182
|
+
end
|
150
183
|
|
151
|
-
def
|
184
|
+
def translate_value_schema(column, col, value)
|
185
|
+
type = schema.column_type(column.to_s)
|
152
186
|
case value
|
153
|
-
when String, Numeric, TrueClass, FalseClass
|
154
|
-
# in Postgres 9.3, you can't compare numeric
|
155
|
-
col.eq(value.to_s)
|
156
|
-
when NilClass
|
157
|
-
# You can't use IS NULL
|
158
|
-
col.eq('')
|
159
187
|
when Hash
|
160
|
-
|
161
|
-
|
162
|
-
|
188
|
+
if has_operator?(value)
|
189
|
+
chain(:and, value.map {|op, val|
|
190
|
+
case op
|
191
|
+
when "$in"
|
192
|
+
case type
|
193
|
+
when "array"
|
194
|
+
chain(:or, val.map { |v| col.matches(%Q[%"#{v}"%]) })
|
195
|
+
else
|
196
|
+
compare_schema(col, val, type, :in)
|
197
|
+
end
|
198
|
+
when "$eq", "$ne", "$gt", "$gte", "$lt", "$lte"
|
199
|
+
compare_schema(col, val, type, OPERATOR_MAP[op])
|
200
|
+
else
|
201
|
+
raise UnsupportedQuery, "Unknown operator #{op}"
|
202
|
+
end
|
203
|
+
})
|
204
|
+
else
|
205
|
+
col.eq(value.to_json)
|
163
206
|
end
|
207
|
+
when String, Numeric, NilClass
|
208
|
+
compare_schema(col, value, type, :eq)
|
209
|
+
else
|
210
|
+
compare_schema(col, value.to_json, type, :eq)
|
211
|
+
end
|
212
|
+
end
|
164
213
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
214
|
+
def translate_value_dynamic(col, value)
|
215
|
+
case value
|
216
|
+
when String, TrueClass, FalseClass
|
217
|
+
col.eq(value.to_s)
|
218
|
+
when Numeric, NilClass
|
219
|
+
compare(col, value, :eq)
|
220
|
+
when Hash
|
221
|
+
if has_operator?(value)
|
222
|
+
chain(:and, value.map {|op, val|
|
223
|
+
case op
|
224
|
+
when "$in"
|
225
|
+
if val.all? {|v| v.is_a? Numeric }
|
226
|
+
wrap(col, val.first).in(val)
|
227
|
+
else
|
228
|
+
col.in(val.map(&:to_s))
|
229
|
+
end
|
230
|
+
when "$eq", "$ne", "$gt", "$gte", "$lt", "$lte"
|
231
|
+
compare(col, val, OPERATOR_MAP[op])
|
232
|
+
else
|
233
|
+
raise UnsupportedQuery, "Unknown operator #{op}"
|
234
|
+
end
|
235
|
+
})
|
236
|
+
else
|
237
|
+
col.eq(value.to_json)
|
177
238
|
end
|
239
|
+
else
|
240
|
+
col.eq(value.to_json)
|
178
241
|
end
|
179
242
|
end
|
180
243
|
|
181
|
-
def
|
244
|
+
def has_operator?(value)
|
245
|
+
value.keys.any? {|key| key =~ /^\$/ }
|
246
|
+
end
|
247
|
+
|
248
|
+
def compare(col, val, op)
|
249
|
+
wrap(col, val).send(op, val)
|
250
|
+
end
|
251
|
+
|
252
|
+
def wrap(col, val)
|
182
253
|
case val
|
183
|
-
when
|
184
|
-
|
185
|
-
|
186
|
-
Arel.sql("(#{col})
|
254
|
+
when NilClass
|
255
|
+
# data#>>'{foo}' IS NULL is invalid
|
256
|
+
# (data#>>'{foo}') IS NULL is valid
|
257
|
+
Arel.sql("(#{col})")
|
258
|
+
when Numeric
|
259
|
+
Arel.sql("(#{col})::numeric")
|
187
260
|
else
|
188
261
|
col
|
189
262
|
end
|
190
263
|
end
|
191
264
|
|
192
|
-
def
|
193
|
-
|
194
|
-
|
195
|
-
|
265
|
+
def compare_schema(col, val, type, op)
|
266
|
+
wrap_schema(col, type).send(op, val)
|
267
|
+
end
|
268
|
+
|
269
|
+
def wrap_schema(col, type)
|
270
|
+
case type
|
271
|
+
when "string"
|
272
|
+
Arel.sql("(#{col})")
|
273
|
+
when "number", "integer"
|
274
|
+
Arel.sql("(#{col})::numeric")
|
196
275
|
else
|
197
|
-
|
276
|
+
col
|
198
277
|
end
|
199
278
|
end
|
200
279
|
|
280
|
+
def sql_json_path(col)
|
281
|
+
paths = col.to_s.split('.')
|
282
|
+
Arel.sql("data#>>#{json_pathize(paths)}")
|
283
|
+
end
|
284
|
+
|
201
285
|
def json_pathize(paths)
|
202
286
|
quote("{#{paths.join(',')}}")
|
203
287
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mongery
|
2
|
+
class Schema
|
3
|
+
VALID_TYPES = ["array", "boolean", "integer", "number", "null", "object", "string"]
|
4
|
+
|
5
|
+
def initialize(schema)
|
6
|
+
@schema = schema
|
7
|
+
end
|
8
|
+
|
9
|
+
def property(col)
|
10
|
+
@schema.properties[col]
|
11
|
+
end
|
12
|
+
|
13
|
+
def column_type(col)
|
14
|
+
value = type_value(col)
|
15
|
+
if VALID_TYPES.include?(value)
|
16
|
+
value
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def type_value(col)
|
23
|
+
type = property(col).try(:type)
|
24
|
+
case type
|
25
|
+
when Array
|
26
|
+
(type - ["null"]).first
|
27
|
+
else
|
28
|
+
type
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/mongery/version.rb
CHANGED
data/mongery.gemspec
CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.7"
|
24
24
|
spec.add_development_dependency "rake", "~> 10.0"
|
25
25
|
spec.add_development_dependency "rspec"
|
26
|
-
spec.add_development_dependency "activerecord"
|
26
|
+
spec.add_development_dependency "activerecord", ">= 4.0.0"
|
27
27
|
spec.add_development_dependency "pg"
|
28
|
+
spec.add_development_dependency "json_schema"
|
28
29
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json_schema'
|
3
|
+
|
4
|
+
describe Mongery::Builder do
|
5
|
+
queries = [
|
6
|
+
[ { name: "foo" },
|
7
|
+
/WHERE \(data#>>'{name}'\) = 'foo'$/ ],
|
8
|
+
[ { weight: { "$gt" => 66 } },
|
9
|
+
/WHERE \(data#>>'{weight}'\)::numeric > 66$/ ],
|
10
|
+
[ { weight: { "$gt" => 66.0 } },
|
11
|
+
/WHERE \(data#>>'{weight}'\)::numeric > 66\.0$/ ],
|
12
|
+
[ { name: { "$in" => ["John", "Bob"] } },
|
13
|
+
/WHERE \(data#>>'{name}'\) IN \('John', 'Bob'\)$/ ],
|
14
|
+
[ { age: { '$ne' => 10 }},
|
15
|
+
/WHERE \(data#>>'{age}'\)::numeric != 10/ ],
|
16
|
+
[ { tag: {"$in" => [ "food", "recipe" ]} },
|
17
|
+
/WHERE \(data#>>'{tag}' ILIKE '%"food"%' OR data#>>'{tag}' ILIKE '%"recipe"%'\)$/ ],
|
18
|
+
[ { active: true },
|
19
|
+
/WHERE data#>>'{active}' = 'true'$/ ],
|
20
|
+
]
|
21
|
+
|
22
|
+
schema = JsonSchema.parse!(JSON.parse(<<-EOF))
|
23
|
+
{
|
24
|
+
"type": "object",
|
25
|
+
"properties": {
|
26
|
+
"name": {
|
27
|
+
"type": ["string", "null"]
|
28
|
+
},
|
29
|
+
"weight":{
|
30
|
+
"type": "number"
|
31
|
+
},
|
32
|
+
"age":{
|
33
|
+
"type": ["integer", "null"]
|
34
|
+
},
|
35
|
+
"tag":{
|
36
|
+
"type": "array"
|
37
|
+
},
|
38
|
+
"active":{
|
39
|
+
"type": "boolean"
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
EOF
|
44
|
+
|
45
|
+
builder = Mongery::Builder.new(:test, ActiveRecord::Base, schema)
|
46
|
+
|
47
|
+
queries.each do |query, sql|
|
48
|
+
context "with query #{query}" do
|
49
|
+
subject do
|
50
|
+
builder.find(query).to_sql
|
51
|
+
end
|
52
|
+
it { should match sql }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Mongery::Builder do
|
4
|
-
|
4
|
+
queries = [
|
5
5
|
[ { }, { },
|
6
6
|
/^SELECT "test"\."data" FROM "test"$/ ],
|
7
7
|
[ { _id: "foo" }, { },
|
@@ -15,56 +15,73 @@ describe Mongery::Builder do
|
|
15
15
|
[ { _id: "foo" }, { skip: 10, sort: { _id: 1 } },
|
16
16
|
/WHERE "test"\."id" = 'foo' ORDER BY "test"\."id" ASC OFFSET 10$/ ],
|
17
17
|
[ { _id: "foo" }, { sort: { name: -1, email: 1 } },
|
18
|
-
/WHERE "test"\."id" = 'foo' ORDER BY data
|
18
|
+
/WHERE "test"\."id" = 'foo' ORDER BY data#>>'{name}' DESC, data#>>'{email}' ASC$/ ],
|
19
19
|
[ { "_id" => "foo" }, { },
|
20
20
|
/WHERE "test"\."id" = 'foo'$/ ],
|
21
21
|
[ { name: "foo" }, { },
|
22
|
-
/WHERE data
|
22
|
+
/WHERE data#>>'{name}' = 'foo'$/ ],
|
23
|
+
[ { name: "" }, { },
|
24
|
+
/WHERE data#>>'{name}' = ''$/ ],
|
23
25
|
[ { name: nil }, { },
|
24
|
-
/WHERE data
|
26
|
+
/WHERE \(data#>>'{name}'\) IS NULL$/ ],
|
25
27
|
[ { name: "foo", other: "bar" }, { },
|
26
|
-
/WHERE data
|
28
|
+
/WHERE data#>>'{name}' = 'foo' AND data#>>'{other}' = 'bar'/ ],
|
27
29
|
[ { "x'y" => "foo" }, { },
|
28
|
-
/WHERE data
|
30
|
+
/WHERE data#>>'{x''y}' = 'foo'/ ],
|
29
31
|
[ { weight: 66 }, { },
|
30
|
-
/WHERE data
|
32
|
+
/WHERE \(data#>>'{weight}'\)::numeric = 66$/ ],
|
31
33
|
[ { weight: { "$gt" => 66 } }, { },
|
32
|
-
/WHERE \(data
|
34
|
+
/WHERE \(data#>>'{weight}'\)::numeric > 66$/ ],
|
33
35
|
[ { weight: { "$gt" => 66.0 } }, { },
|
34
|
-
/WHERE \(data
|
36
|
+
/WHERE \(data#>>'{weight}'\)::numeric > 66\.0$/ ],
|
35
37
|
[ { weight: { "$lte" => 66 } }, { },
|
36
|
-
/WHERE \(data
|
38
|
+
/WHERE \(data#>>'{weight}'\)::numeric <= 66$/ ],
|
37
39
|
[ { age: { '$eq' => 10 }}, { },
|
38
|
-
/WHERE \(data
|
40
|
+
/WHERE \(data#>>'{age}'\)::numeric = 10/ ],
|
39
41
|
[ { age: { '$ne' => 10 }}, { },
|
40
|
-
/WHERE \(data
|
42
|
+
/WHERE \(data#>>'{age}'\)::numeric != 10/ ],
|
41
43
|
[ { bool: true }, { },
|
42
|
-
/WHERE data
|
44
|
+
/WHERE data#>>'{bool}' = 'true'$/ ],
|
43
45
|
[ { 'email.address' => 'john@example.com' }, { },
|
44
46
|
/WHERE data#>>'{email,address}' = 'john@example.com'$/ ],
|
45
47
|
[ { "x'y.z" => 'john' }, { },
|
46
48
|
/WHERE data#>>'{x''y,z}' = 'john'$/ ],
|
47
49
|
[ { type: "food", "$or" => [{name: "miso"}, {name: "tofu"}]}, { },
|
48
|
-
/WHERE data
|
50
|
+
/WHERE data#>>'{type}' = 'food' AND \(data#>>'{name}' = 'miso' OR data#>>'{name}' = 'tofu'\)$/ ],
|
49
51
|
[ { "$or" => [{ _id: "foo" }, { _id: "bar" }] }, { },
|
50
52
|
/WHERE \("test"\."id" = 'foo' OR "test"\."id" = 'bar'\)$/ ],
|
51
53
|
[ { "$or" => [{ name: "John" }, { weight: 120 }] }, { },
|
52
|
-
/WHERE \(data
|
54
|
+
/WHERE \(data#>>'{name}' = 'John' OR \(data#>>'{weight}'\)::numeric = 120\)$/ ],
|
53
55
|
[ { "$and" => [{ _id: "foo" }, { name: "bar" }] }, { },
|
54
|
-
/WHERE "test"\."id" = 'foo' AND data
|
56
|
+
/WHERE "test"\."id" = 'foo' AND data#>>'{name}' = 'bar'$/ ],
|
55
57
|
[ { "$and" => [{ name: "John" }, { weight: 120 }] }, { },
|
56
|
-
/WHERE data
|
58
|
+
/WHERE data#>>'{name}' = 'John' AND \(data#>>'{weight}'\)::numeric = 120$/ ],
|
57
59
|
[ { "$and" => [{ "$or" => [{name: "John"}, {email: "john"}] }, {_id: "Bob"}] }, { },
|
58
|
-
/WHERE \(data
|
59
|
-
[ {
|
60
|
-
/WHERE data
|
61
|
-
[ {
|
62
|
-
/WHERE \(data
|
60
|
+
/WHERE \(data#>>'{name}' = 'John' OR data#>>'{email}' = 'john'\) AND "test"\."id" = 'Bob'$/ ],
|
61
|
+
[ { bar: {"$in" => [ "foo" ]} }, { },
|
62
|
+
/WHERE data#>>'{bar}' IN \('foo'\)$/ ],
|
63
|
+
[ { tag: {"$in" => [ 1, 2 ]} }, { },
|
64
|
+
/WHERE \(data#>>'{tag}'\)::numeric IN \(1, 2\)$/ ],
|
65
|
+
[ { bar: {"$in" => [ "foo", "bar" ]} }, { },
|
66
|
+
/WHERE data#>>'{bar}' IN \('foo', 'bar'\)$/ ],
|
67
|
+
[ { bar: {"$in" => [ "foo", 1.2 ]} }, { },
|
68
|
+
/WHERE data#>>'{bar}' IN \('foo', '1\.2'\)$/ ],
|
69
|
+
[ { bar: { foo: "bar", baz: [1,2,3]} }, { },
|
70
|
+
/WHERE data#>>'{bar}' = '{"foo":"bar","baz":\[1,2,3\]}'$/ ],
|
71
|
+
[ { "foo.bar" => true }, { },
|
72
|
+
/WHERE data#>>'{foo,bar}' = 'true'$/ ],
|
73
|
+
]
|
74
|
+
|
75
|
+
bad_queries = [
|
76
|
+
{ "$bar" => "foo" },
|
77
|
+
{ "_id" => { "$bar" => 1 } },
|
78
|
+
{ "foo" => { "$bar" => 1 } },
|
79
|
+
{ "foo" => { "$lt" => 1, "gt" => 2 } },
|
63
80
|
]
|
64
81
|
|
65
82
|
builder = Mongery::Builder.new(:test)
|
66
83
|
|
67
|
-
|
84
|
+
queries.each do |query, condition, sql|
|
68
85
|
context "with query #{query}" do
|
69
86
|
subject do
|
70
87
|
builder.find(query).tap { |q|
|
@@ -76,4 +93,13 @@ describe Mongery::Builder do
|
|
76
93
|
it { should match sql }
|
77
94
|
end
|
78
95
|
end
|
96
|
+
|
97
|
+
bad_queries.each do |query|
|
98
|
+
context "with query #{query}" do
|
99
|
+
it "should throw UnsupportedQuery exception" do
|
100
|
+
expect { builder.find(query).to_sql }
|
101
|
+
.to raise_error(Mongery::UnsupportedQuery)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
79
105
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongery
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tatsuhiko Miyagawa
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: arel
|
@@ -68,6 +68,20 @@ dependencies:
|
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.0.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.0.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
87
|
- - ">="
|
@@ -81,7 +95,7 @@ dependencies:
|
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
98
|
+
name: json_schema
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
101
|
- - ">="
|
@@ -108,8 +122,10 @@ files:
|
|
108
122
|
- README.md
|
109
123
|
- Rakefile
|
110
124
|
- lib/mongery.rb
|
125
|
+
- lib/mongery/schema.rb
|
111
126
|
- lib/mongery/version.rb
|
112
127
|
- mongery.gemspec
|
128
|
+
- spec/mongery/builder_schema_spec.rb
|
113
129
|
- spec/mongery/builder_spec.rb
|
114
130
|
- spec/spec_helper.rb
|
115
131
|
homepage: https://github.com/miyagawa/mongery
|
@@ -132,11 +148,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
148
|
version: '0'
|
133
149
|
requirements: []
|
134
150
|
rubyforge_project:
|
135
|
-
rubygems_version: 2.
|
151
|
+
rubygems_version: 2.4.3
|
136
152
|
signing_key:
|
137
153
|
specification_version: 4
|
138
154
|
summary: Convert MongoDB query to Arel for PostgreSQL + JSON
|
139
155
|
test_files:
|
156
|
+
- spec/mongery/builder_schema_spec.rb
|
140
157
|
- spec/mongery/builder_spec.rb
|
141
158
|
- spec/spec_helper.rb
|
142
159
|
has_rdoc:
|