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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1124d4cc8a8887a26daf6cd0167b395692eb2b80
4
- data.tar.gz: e0cc0961e38626e40cc26f079471e0e0c52186c2
3
+ metadata.gz: f07b3b2676098909d2b127f75970b95b4e4be027
4
+ data.tar.gz: 9d5c86b70c843e233ebeb5d101c44e6f9ddef6ae
5
5
  SHA512:
6
- metadata.gz: e4de70968015998d26f1bd15e15e8e08c24a67356ae1ba3351f1be7ca6baed823a8c7688bab266b3fddb7dd9a31cbdfbb5540b1e81d06235bea504ab5e294409
7
- data.tar.gz: 8861688d905bb127308cde48cd9797903d00620f500c15d89413527a20fce2bca18262af44761ee237611eb4923576f0c7dbf0abc33a414c8b21a096cf13454b
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. Index using hstore columns and/or 9.4 `jsonb` types are planned but not implemented.
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->>'age')::integer >= 21 ORDER BY data->>'name' DESC
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->>'age')::integer >= 21
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->>'age')::integer = 21
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 { |col, value| translate_cv(col, value) })
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 { |q| translate(q) })
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
- translate_value_json(sql_json_path(col), value)
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
- def translate_value(col, value, json = false)
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
- ops = value.keys
127
- if ops.size > 1
128
- raise UnsupportedQuery, "Multiple operator supported: #{ops.join(", ")}"
129
- end
130
-
131
- val = value[ops.first]
132
- case ops.first
133
- when "$in"
134
- col.in(val)
135
- when "$gt", "$gte", "$lt", "$lte"
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
- COMPARE_MAPS = { "$gt" => :gt, "$gte" => :gteq, "$lt" => :lt, "$lte" => :lteq }
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 translate_value_json(col, value, json = false)
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
- ops = value.keys
161
- if ops.size > 1
162
- raise UnsupportedQuery, "Multiple operator supported: #{ops.join(", ")}"
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
- val = value[ops.first]
166
- case ops.first
167
- when "$in"
168
- chain(:or, val.map { |val| col.matches(%Q[%"#{val}"%]) })
169
- when "$gt", "$gte", "$lt", "$lte"
170
- wrap_numeric(col, val).send(COMPARE_MAPS[ops.first], val)
171
- when "$eq"
172
- wrap_numeric(col, val).eq(val)
173
- when "$ne"
174
- wrap_numeric(col, val).not_eq(val)
175
- when /^\$/
176
- raise UnsupportedQuery, "Unknown operator #{ops.first}"
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 wrap_numeric(col, val)
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 Float
184
- Arel.sql("(#{col})::float")
185
- when Integer
186
- Arel.sql("(#{col})::integer")
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 sql_json_path(col)
193
- paths = col.to_s.split('.')
194
- if paths.size > 1
195
- Arel.sql("data#>>#{json_pathize(paths)}")
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
- Arel.sql("data->>#{quote(paths.first)}")
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
@@ -1,3 +1,3 @@
1
1
  module Mongery
2
- VERSION = "0.0.5"
2
+ VERSION = "1.0.0"
3
3
  end
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
- tests = [
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->>'name' DESC, data->>'email' ASC$/ ],
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->>'name' = 'foo'$/ ],
22
+ /WHERE data#>>'{name}' = 'foo'$/ ],
23
+ [ { name: "" }, { },
24
+ /WHERE data#>>'{name}' = ''$/ ],
23
25
  [ { name: nil }, { },
24
- /WHERE data->>'name' = ''$/ ],
26
+ /WHERE \(data#>>'{name}'\) IS NULL$/ ],
25
27
  [ { name: "foo", other: "bar" }, { },
26
- /WHERE data->>'name' = 'foo' AND data->>'other' = 'bar'/ ],
28
+ /WHERE data#>>'{name}' = 'foo' AND data#>>'{other}' = 'bar'/ ],
27
29
  [ { "x'y" => "foo" }, { },
28
- /WHERE data->>'x''y' = 'foo'/ ],
30
+ /WHERE data#>>'{x''y}' = 'foo'/ ],
29
31
  [ { weight: 66 }, { },
30
- /WHERE data->>'weight' = '66'$/ ],
32
+ /WHERE \(data#>>'{weight}'\)::numeric = 66$/ ],
31
33
  [ { weight: { "$gt" => 66 } }, { },
32
- /WHERE \(data->>'weight'\)::integer > 66$/ ],
34
+ /WHERE \(data#>>'{weight}'\)::numeric > 66$/ ],
33
35
  [ { weight: { "$gt" => 66.0 } }, { },
34
- /WHERE \(data->>'weight'\)::float > 66\.0$/ ],
36
+ /WHERE \(data#>>'{weight}'\)::numeric > 66\.0$/ ],
35
37
  [ { weight: { "$lte" => 66 } }, { },
36
- /WHERE \(data->>'weight'\)::integer <= 66$/ ],
38
+ /WHERE \(data#>>'{weight}'\)::numeric <= 66$/ ],
37
39
  [ { age: { '$eq' => 10 }}, { },
38
- /WHERE \(data->>'age'\)::integer = 10/ ],
40
+ /WHERE \(data#>>'{age}'\)::numeric = 10/ ],
39
41
  [ { age: { '$ne' => 10 }}, { },
40
- /WHERE \(data->>'age'\)::integer != 10/ ],
42
+ /WHERE \(data#>>'{age}'\)::numeric != 10/ ],
41
43
  [ { bool: true }, { },
42
- /WHERE data->>'bool' = 'true'$/ ],
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->>'type' = 'food' AND \(data->>'name' = 'miso' OR data->>'name' = 'tofu'\)$/ ],
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->>'name' = 'John' OR data->>'weight' = '120'\)$/ ],
54
+ /WHERE \(data#>>'{name}' = 'John' OR \(data#>>'{weight}'\)::numeric = 120\)$/ ],
53
55
  [ { "$and" => [{ _id: "foo" }, { name: "bar" }] }, { },
54
- /WHERE "test"\."id" = 'foo' AND data->>'name' = 'bar'$/ ],
56
+ /WHERE "test"\."id" = 'foo' AND data#>>'{name}' = 'bar'$/ ],
55
57
  [ { "$and" => [{ name: "John" }, { weight: 120 }] }, { },
56
- /WHERE data->>'name' = 'John' AND data->>'weight' = '120'$/ ],
58
+ /WHERE data#>>'{name}' = 'John' AND \(data#>>'{weight}'\)::numeric = 120$/ ],
57
59
  [ { "$and" => [{ "$or" => [{name: "John"}, {email: "john"}] }, {_id: "Bob"}] }, { },
58
- /WHERE \(data->>'name' = 'John' OR data->>'email' = 'john'\) AND "test"\."id" = 'Bob'$/ ],
59
- [ { ids: {"$in" => [ "foo" ]} }, { },
60
- /WHERE data->>'ids' ILIKE '%"foo"%'$/ ],
61
- [ { ids: {"$in" => [ "foo", "bar" ]} }, { },
62
- /WHERE \(data->>'ids' ILIKE '%"foo"%' OR data->>'ids' ILIKE '%"bar"%'\)$/ ],
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
- tests.each do |query, condition, sql|
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.5
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-11 00:00:00.000000000 Z
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: pg
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.2.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: