mongo_ql 0.0.4 → 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/.vscode/launch.json +16 -0
- data/README.md +82 -1
- data/Rakefile +10 -0
- data/debug.sh +2 -0
- data/design_specs.md +70 -62
- data/lib/mongo_ql/collection_operators.rb +9 -5
- data/lib/mongo_ql/expression/binary.rb +1 -1
- data/lib/mongo_ql/expression/method_call.rb +4 -4
- data/lib/mongo_ql/expression/unary.rb +1 -1
- data/lib/mongo_ql/expression/value_node.rb +18 -0
- data/lib/mongo_ql/expression.rb +1 -1
- data/lib/mongo_ql/stage/add_fields.rb +25 -0
- data/lib/mongo_ql/stage/group.rb +6 -5
- data/lib/mongo_ql/stage/lookup.rb +9 -5
- data/lib/mongo_ql/stage/match.rb +11 -9
- data/lib/mongo_ql/stage/project.rb +6 -13
- data/lib/mongo_ql/stage/sort.rb +3 -2
- data/lib/mongo_ql/stage/unwind.rb +6 -4
- data/lib/mongo_ql/stage.rb +9 -1
- data/lib/mongo_ql/stage_context.rb +10 -14
- data/lib/mongo_ql/string_operators.rb +3 -3
- data/lib/mongo_ql/utils.rb +29 -0
- data/lib/mongo_ql/version.rb +1 -1
- data/lib/mongo_ql.rb +19 -6
- data/mongo_ql.gemspec +3 -2
- metadata +25 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1f6676de369add0c89989cb95013bd170cc5925959f50b74ee1a7824e148b30
|
4
|
+
data.tar.gz: 0cd46af4aa9fa2b9a98e10e6ba7532c3aef216e3ca2d3af983b739db2fec2237
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc76501d7a5e6e1a13a58fd4164de221f5038afd7006aa7cd8b7b1ec2b4792d278e2797b2d4d0daa117779dd63b4bf34a8c7d5c8b9d110d0ec1c8f3bedde5fd2
|
7
|
+
data.tar.gz: 2ec8a62fd293bcd9fc8d266424f18a3206db5be9a8e16ebcb4eb2d9e2679bfbab39e54fd64287440f6235463989a5bae220976dad4ee615b89c5b86d8b29ebb7
|
data/.vscode/launch.json
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
3
|
+
// Hover to view descriptions of existing attributes.
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
5
|
+
"version": "0.2.0",
|
6
|
+
"configurations": [
|
7
|
+
{
|
8
|
+
"name": "Listen for rdebug-ide",
|
9
|
+
"type": "Ruby",
|
10
|
+
"request": "attach",
|
11
|
+
"remoteHost": "127.0.0.1",
|
12
|
+
"remotePort": "1234",
|
13
|
+
"remoteWorkspaceRoot": "${workspaceRoot}"
|
14
|
+
}
|
15
|
+
]
|
16
|
+
}
|
data/README.md
CHANGED
@@ -1 +1,82 @@
|
|
1
|
-
|
1
|
+
|
2
|
+
# Query DSL
|
3
|
+
```ruby
|
4
|
+
Order.where { tax != 0 }
|
5
|
+
#=> Order.where(tax: { "$ne": 0 })
|
6
|
+
Order.where { total >= 100 }
|
7
|
+
#=> Order.where(total: { "$gte": 100 })
|
8
|
+
Order.where { (total >= 100) & (total_tax < 15) }
|
9
|
+
#=> Order.where({ "$and": [{ total: { "$gte": 100 }}, { total_tax: { "$lt": 15 }}]})
|
10
|
+
Order.where { tax > (shipping / 2) }
|
11
|
+
#=> Order.where(tax: { "$gt": { "$divide": [ "$shipping", 2]}})
|
12
|
+
Order.where { total >= If(currency == "CAD", 100, 80) }
|
13
|
+
#=> Order.where(total: { "$cond": { if: { "$eq": ["$currency", "CAD"]}, then: 100, else: 80 }})
|
14
|
+
```
|
15
|
+
|
16
|
+
# Aggregation Pipeline DSL
|
17
|
+
```ruby
|
18
|
+
Order.all.mongo_ql do
|
19
|
+
join Customer,
|
20
|
+
on: customer_id == _id.to_id,
|
21
|
+
as: customers
|
22
|
+
|
23
|
+
join Shipping, :as => shippings do |doc|
|
24
|
+
match order_id == doc._id,
|
25
|
+
status == :shipped
|
26
|
+
end
|
27
|
+
|
28
|
+
match province == "ON"
|
29
|
+
|
30
|
+
project _id,
|
31
|
+
total,
|
32
|
+
customer => customers.name,
|
33
|
+
tax => total * tax_rate
|
34
|
+
|
35
|
+
group customer,
|
36
|
+
total => total.sum,
|
37
|
+
total_tax => tax.sum * 5
|
38
|
+
|
39
|
+
sort_by age.desc
|
40
|
+
end
|
41
|
+
|
42
|
+
# The above aggregation is equivalent to the following mognodb pipeline
|
43
|
+
Order.collection.pipeline([
|
44
|
+
{ "$lookup": {
|
45
|
+
from: "customers",
|
46
|
+
localField: "$customer_id",
|
47
|
+
foreignField: "$_id",
|
48
|
+
as: "customers"
|
49
|
+
}},
|
50
|
+
{ "$unwind": {
|
51
|
+
path: "customers"
|
52
|
+
}},
|
53
|
+
{ "$lookup": {
|
54
|
+
from: "shippings",
|
55
|
+
as: "shippings",
|
56
|
+
let: { doc_id: "$_id" },
|
57
|
+
pipeline: [{
|
58
|
+
"$match": {
|
59
|
+
order_id: { "$eq": "$$dock_id" },
|
60
|
+
status: :shipped
|
61
|
+
}
|
62
|
+
}]
|
63
|
+
}},
|
64
|
+
{ "$unwind": {
|
65
|
+
path: "customers"
|
66
|
+
}},
|
67
|
+
{ "$match": {
|
68
|
+
province: { "$eq": "ON" }
|
69
|
+
}},
|
70
|
+
{ "$project": {
|
71
|
+
_id: 1,
|
72
|
+
total: 1,
|
73
|
+
customer: "$customers.name",
|
74
|
+
tax: { "$multiply": ["$total", "$tax_rate"] }
|
75
|
+
}},
|
76
|
+
{ "$group": {
|
77
|
+
_id: "$customer",
|
78
|
+
total: { "$sum": "$total" },
|
79
|
+
total_tax: { "$multiply": [{ "$sum": "$tax" }, 5] }
|
80
|
+
}}
|
81
|
+
])
|
82
|
+
```
|
data/Rakefile
ADDED
data/debug.sh
ADDED
data/design_specs.md
CHANGED
@@ -1,26 +1,12 @@
|
|
1
1
|
|
2
|
-
# Query DSL
|
3
|
-
```ruby
|
4
|
-
Order.where { tax != 0 }
|
5
|
-
#=> Order.where(tax: { "$ne": 0 })
|
6
|
-
Order.where { total >= 100 }
|
7
|
-
#=> Order.where(total: { "$gte": 100 })
|
8
|
-
Order.where { (total >= 100) & (total_tax < 15) }
|
9
|
-
#=> Order.where({ "$and": [{ total: { "$gte": 100 }}, { total_tax: { "$lt": 15 }}]})
|
10
|
-
Order.where { tax > (shipping / 2) }
|
11
|
-
#=> Order.where(tax: { "$gt": { "$divide": [ "$shipping", 2]}})
|
12
|
-
Order.where { total >= If(currency == "CAD", 100, 80) }
|
13
|
-
#=> Order.where(total: { "$cond": { if: { "$eq": ["$currency", "CAD"]}, then: 100, else: 80 }})
|
14
|
-
```
|
15
|
-
|
16
2
|
# Aggregation Pipeline DSL
|
17
3
|
```ruby
|
18
|
-
|
19
|
-
join
|
4
|
+
MongoQL.compose do
|
5
|
+
join customers,
|
20
6
|
on: customer_id == _id.to_id,
|
21
7
|
as: customers
|
22
8
|
|
23
|
-
join
|
9
|
+
join shippings, :as => shippings do |doc|
|
24
10
|
match order_id == doc._id,
|
25
11
|
status == :shipped
|
26
12
|
end
|
@@ -28,58 +14,80 @@ Order.all.mongo_ql do
|
|
28
14
|
match province == "ON"
|
29
15
|
|
30
16
|
project :_id,
|
31
|
-
|
32
|
-
|
33
|
-
|
17
|
+
total,
|
18
|
+
customer => customers.name,
|
19
|
+
tax => total * tax_rate
|
34
20
|
|
35
21
|
group customer,
|
36
|
-
|
37
|
-
|
22
|
+
total => total.sum,
|
23
|
+
total_tax => tax.sum * 5
|
38
24
|
|
39
25
|
sort_by age.desc
|
40
|
-
|
41
|
-
page 1
|
42
|
-
per 10
|
43
26
|
end
|
27
|
+
```
|
44
28
|
|
45
29
|
# The above aggregation is equivalent to the following mognodb pipeline
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
30
|
+
```json
|
31
|
+
[{
|
32
|
+
"$lookup": {
|
33
|
+
"from": "customers",
|
34
|
+
"as": "customers",
|
35
|
+
"localField": "customer_id",
|
36
|
+
"foreignField": {
|
37
|
+
"$toString": {
|
38
|
+
"$toObjectId": "$_id"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}, {
|
43
|
+
"$lookup": {
|
44
|
+
"from": "shippings",
|
45
|
+
"as": "shippings",
|
46
|
+
"pipeline": [{
|
61
47
|
"$match": {
|
62
|
-
|
63
|
-
|
48
|
+
"$expr": {
|
49
|
+
"$and": [{
|
50
|
+
"$eq": ["$order_id", "$$var__id"]
|
51
|
+
}, {
|
52
|
+
"$eq": ["$status", "shipped"]
|
53
|
+
}]
|
54
|
+
}
|
64
55
|
}
|
65
|
-
}]
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
}
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
56
|
+
}],
|
57
|
+
"let": {
|
58
|
+
"var__id": "$_id"
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}, {
|
62
|
+
"$match": {
|
63
|
+
"$expr": {
|
64
|
+
"$eq": ["$province", "ON"]
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}, {
|
68
|
+
"$project": {
|
69
|
+
"_id": 1,
|
70
|
+
"total": 1,
|
71
|
+
"customer": "customers",
|
72
|
+
"tax": {
|
73
|
+
"$multiply": ["$total", "$tax_rate"]
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}, {
|
77
|
+
"$group": {
|
78
|
+
"_id": "$customer",
|
79
|
+
"total": {
|
80
|
+
"$sum": "$total"
|
81
|
+
},
|
82
|
+
"total_tax": {
|
83
|
+
"$multiply": [{
|
84
|
+
"$sum": "$tax"
|
85
|
+
}, 5]
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}, {
|
89
|
+
"$sort": {
|
90
|
+
"age.desc": 1
|
91
|
+
}
|
92
|
+
}]
|
85
93
|
```
|
@@ -22,43 +22,47 @@ module MongoQL
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def filter(&block)
|
25
|
+
evaled_cond = block.call(Expression::FieldNode.new("$item"))
|
25
26
|
Expression::MethodCall.new "$filter", self, ast_template: -> (target, **_args) {
|
26
27
|
{
|
27
28
|
"input" => target,
|
28
29
|
"as" => "item",
|
29
|
-
"cond" =>
|
30
|
+
"cond" => evaled_cond
|
30
31
|
}
|
31
32
|
}
|
32
33
|
end
|
33
34
|
|
34
35
|
def map(&block)
|
36
|
+
evaled_in = block.call(Expression::FieldNode.new("$item"))
|
35
37
|
Expression::MethodCall.new "$map", self, ast_template: -> (target, **_args) {
|
36
38
|
{
|
37
39
|
"input" => target,
|
38
40
|
"as" => "item",
|
39
|
-
"in" =>
|
41
|
+
"in" => evaled_in
|
40
42
|
}
|
41
43
|
}
|
42
44
|
end
|
43
45
|
|
44
46
|
def reduce(initial_value, &block)
|
47
|
+
evaled_in = to_expression(block.call(Expression::FieldNode.new("$value"), Expression::FieldNode.new("$this")))
|
45
48
|
Expression::MethodCall.new "$reduce", self, ast_template: -> (target, **_args) {
|
46
49
|
{
|
47
50
|
"input" => target,
|
48
|
-
"initialValue" => to_expression(initial_value)
|
49
|
-
"in" =>
|
51
|
+
"initialValue" => to_expression(initial_value),
|
52
|
+
"in" => evaled_in
|
50
53
|
}
|
51
54
|
}
|
52
55
|
end
|
53
56
|
|
54
57
|
def contains(ele)
|
55
58
|
Expression::MethodCall.new "$in", self, ast_template: -> (target, **_args) {
|
56
|
-
[to_expression(ele)
|
59
|
+
[to_expression(ele), target]
|
57
60
|
}
|
58
61
|
end
|
59
62
|
alias_method :includes, :contains
|
60
63
|
alias_method :include, :contains
|
61
64
|
alias_method :include?, :contains
|
65
|
+
alias_method :includes?, :contains
|
62
66
|
|
63
67
|
end
|
64
68
|
end
|
@@ -5,17 +5,17 @@ module MongoQL
|
|
5
5
|
attr_accessor :method, :target, :ast_template, :args
|
6
6
|
|
7
7
|
def initialize(method, target, ast_template: nil, **args)
|
8
|
-
@target = to_expression(target)
|
9
8
|
@method = method
|
10
|
-
@ast_template = ast_template
|
11
9
|
@args = args
|
10
|
+
@target = to_expression(target)
|
11
|
+
@ast_template = ast_template
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_ast
|
15
15
|
if ast_template
|
16
|
-
{ method => ast_template.call(target
|
16
|
+
{ method => ast_template.call(target, **args) }
|
17
17
|
else
|
18
|
-
{ method => target
|
18
|
+
{ method => target }
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -2,14 +2,32 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Expression::ValueNode < Expression
|
5
|
+
SUPPORTED_TYPES = [
|
6
|
+
String, Integer, Float,
|
7
|
+
Array, Hash, TrueClass,
|
8
|
+
FalseClass, Date, Symbol,
|
9
|
+
MongoQL::Expression::ValueNode
|
10
|
+
].freeze
|
11
|
+
|
5
12
|
attr_accessor :value
|
6
13
|
|
7
14
|
def initialize(val)
|
15
|
+
Expression::ValueNode.valid!(val)
|
8
16
|
@value = val
|
9
17
|
end
|
10
18
|
|
11
19
|
def to_ast
|
12
20
|
value
|
13
21
|
end
|
22
|
+
|
23
|
+
def self.valid?(value)
|
24
|
+
SUPPORTED_TYPES.any? { |type| value.is_a?(type) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.valid!(value)
|
28
|
+
unless valid?(value)
|
29
|
+
raise InvalidValueExpression, "#{value} must be in type #{SUPPORTED_TYPES.map(&:name).join(",")}"
|
30
|
+
end
|
31
|
+
end
|
14
32
|
end
|
15
33
|
end
|
data/lib/mongo_ql/expression.rb
CHANGED
@@ -43,7 +43,7 @@ module MongoQL
|
|
43
43
|
|
44
44
|
def if_null(default_val)
|
45
45
|
Expression::MethodCall.new "$ifNull", self, ast_template: -> (target, **_args) {
|
46
|
-
[target, to_expression(default_val)
|
46
|
+
[target, to_expression(default_val)]
|
47
47
|
}
|
48
48
|
end
|
49
49
|
alias_method :default, :if_null
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MongoQL
|
4
|
+
class Stage::AddFields < Stage
|
5
|
+
attr_accessor :ctx
|
6
|
+
attr_accessor :field_projections
|
7
|
+
|
8
|
+
def initialize(ctx, *fields)
|
9
|
+
@ctx = ctx
|
10
|
+
@field_projections = fields.map do |field|
|
11
|
+
case field
|
12
|
+
when Hash
|
13
|
+
field.map { |k, v| [k.to_s, to_expression(v)] }.to_h
|
14
|
+
else
|
15
|
+
raise ArgumentError, "#{field} is not a valid field mapping option"
|
16
|
+
end
|
17
|
+
end.inject({}) { |p, c| p.merge(c) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_ast
|
21
|
+
ast = { "$addFields" => field_projections }
|
22
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/mongo_ql/stage/group.rb
CHANGED
@@ -2,21 +2,22 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Stage::Group < Stage
|
5
|
-
|
6
|
-
|
5
|
+
attr_accessor :ctx
|
7
6
|
attr_accessor :by, :fields
|
8
7
|
|
9
|
-
def initialize(by, arrow_fields = {}, **fields)
|
8
|
+
def initialize(ctx, by, arrow_fields = {}, **fields)
|
9
|
+
@ctx = ctx
|
10
10
|
@by = by
|
11
11
|
@fields = fields.transform_keys(&:to_s).merge(arrow_fields.transform_keys(&:to_s))
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_ast
|
15
|
-
{
|
15
|
+
ast = {
|
16
16
|
"$group" => {
|
17
17
|
"_id" => by.to_ast,
|
18
|
-
}.merge(fields
|
18
|
+
}.merge(fields)
|
19
19
|
}
|
20
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
end
|
@@ -32,15 +32,18 @@ module MongoQL
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
attr_accessor :from, :condition, :as,
|
35
|
+
attr_accessor :ctx, :from, :condition, :as,
|
36
|
+
:nested_pipeline_block, :let_vars, :nested_pipeline
|
36
37
|
|
37
|
-
def initialize(from, condition = nil, on: nil, as: nil, &block)
|
38
|
+
def initialize(ctx, from, condition = nil, on: nil, as: nil, &block)
|
39
|
+
@ctx = ctx
|
38
40
|
@from = collection_name(from)
|
39
41
|
@as = new_array_name(as)
|
40
42
|
@nested_pipeline_block = block
|
41
43
|
|
42
44
|
if has_nested_pipeline?
|
43
45
|
@let_vars = NestedPipelineVars.new
|
46
|
+
@nested_pipeline = eval_nested_pipeline
|
44
47
|
else
|
45
48
|
@condition = condition_ast(condition || on)
|
46
49
|
end
|
@@ -49,12 +52,13 @@ module MongoQL
|
|
49
52
|
def to_ast
|
50
53
|
lookup_expr = { "from" => from, "as" => as }
|
51
54
|
if has_nested_pipeline?
|
52
|
-
lookup_expr["pipeline"] = nested_pipeline
|
55
|
+
lookup_expr["pipeline"] = nested_pipeline
|
53
56
|
lookup_expr["let"] = let_vars.vars
|
54
57
|
else
|
55
58
|
lookup_expr = lookup_expr.merge(condition)
|
56
59
|
end
|
57
|
-
{ "$lookup" => lookup_expr }
|
60
|
+
ast = { "$lookup" => lookup_expr }
|
61
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
58
62
|
end
|
59
63
|
|
60
64
|
private
|
@@ -62,7 +66,7 @@ module MongoQL
|
|
62
66
|
condition.nil? && !nested_pipeline_block.nil?
|
63
67
|
end
|
64
68
|
|
65
|
-
def
|
69
|
+
def eval_nested_pipeline
|
66
70
|
sub_ctx = StageContext.new
|
67
71
|
sub_ctx.instance_exec(let_vars, &nested_pipeline_block)
|
68
72
|
sub_ctx
|
data/lib/mongo_ql/stage/match.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Stage::Match < Stage
|
5
|
-
attr_accessor :conditions, :field_filters
|
5
|
+
attr_accessor :ctx, :conditions, :field_filters
|
6
6
|
|
7
|
-
def initialize(*conds, **field_filters)
|
7
|
+
def initialize(ctx, *conds, **field_filters)
|
8
|
+
@ctx = ctx
|
8
9
|
conds.each do |c|
|
9
|
-
raise ArgumentError, "#{c.inspect} is not a MongoQL::Expression" unless c.is_a?(MongoQL::Expression)
|
10
|
+
raise ArgumentError, "#{c.inspect} is not a valid MongoQL::Expression" unless c.is_a?(MongoQL::Expression)
|
10
11
|
end
|
11
12
|
@conditions = conds
|
12
13
|
@field_filters = field_filters
|
@@ -14,18 +15,19 @@ module MongoQL
|
|
14
15
|
|
15
16
|
def to_ast
|
16
17
|
conds = {}
|
17
|
-
if
|
18
|
-
conds["$expr"] =
|
18
|
+
if compose_conditions
|
19
|
+
conds["$expr"] = compose_conditions
|
19
20
|
end
|
20
|
-
{ "$match" => conds.merge(field_filters) }
|
21
|
+
ast = { "$match" => conds.merge(field_filters) }
|
22
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
21
23
|
end
|
22
24
|
|
23
25
|
private
|
24
|
-
def
|
26
|
+
def compose_conditions
|
25
27
|
if conditions.size > 1
|
26
|
-
{ "$and" => conditions
|
28
|
+
{ "$and" => conditions }
|
27
29
|
elsif conditions.size == 1
|
28
|
-
conditions[0]
|
30
|
+
conditions[0]
|
29
31
|
else
|
30
32
|
nil
|
31
33
|
end
|
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Stage::Project < Stage
|
5
|
-
attr_accessor :field_projections
|
5
|
+
attr_accessor :ctx, :field_projections
|
6
6
|
|
7
|
-
def initialize(*fields)
|
7
|
+
def initialize(ctx, *fields)
|
8
|
+
@ctx = ctx
|
8
9
|
@field_projections = fields.map do |field|
|
9
10
|
case field
|
10
11
|
when String, Symbol, Expression::FieldNode
|
11
12
|
{ field.to_s => 1 }
|
12
13
|
when Hash
|
13
|
-
field.map { |k, v| [k.to_s, to_expression(v)
|
14
|
+
field.map { |k, v| [k.to_s, to_expression(v)] }.to_h
|
14
15
|
else
|
15
16
|
raise ArgumentError, "#{field} is not a valid field mapping option"
|
16
17
|
end
|
@@ -18,16 +19,8 @@ module MongoQL
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def to_ast
|
21
|
-
{ "$project" => field_projections }
|
22
|
+
ast = { "$project" => field_projections }
|
23
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
22
24
|
end
|
23
|
-
|
24
|
-
protected
|
25
|
-
def to_expression(val)
|
26
|
-
if val.is_a?(Expression)
|
27
|
-
val
|
28
|
-
else
|
29
|
-
Expression::ValueNode.new(val)
|
30
|
-
end
|
31
|
-
end
|
32
25
|
end
|
33
26
|
end
|
data/lib/mongo_ql/stage/sort.rb
CHANGED
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Stage::Unwind < Stage
|
5
|
-
attr_accessor :path, :allow_null
|
5
|
+
attr_accessor :ctx, :path, :allow_null
|
6
6
|
|
7
|
-
def initialize(path, allow_null: false)
|
8
|
-
@
|
7
|
+
def initialize(ctx, path, allow_null: false)
|
8
|
+
@ctx = ctx
|
9
|
+
@path = to_expression(path)
|
9
10
|
@allow_null = allow_null
|
10
11
|
end
|
11
12
|
|
12
13
|
def to_ast
|
13
|
-
{
|
14
|
+
ast = {
|
14
15
|
"$unwind" => {
|
15
16
|
"path" => path,
|
16
17
|
"preserveNullAndEmptyArrays" => allow_null
|
17
18
|
}
|
18
19
|
}
|
20
|
+
MongoQL::Utils.deep_transform_values(ast, &MongoQL::EXPRESSION_TO_AST_MAPPER)
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
data/lib/mongo_ql/stage.rb
CHANGED
@@ -2,9 +2,17 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class Stage
|
5
|
-
|
6
5
|
def to_ast
|
7
6
|
raise NotImplementedError, "stage #{self.class} must implement to_ast"
|
8
7
|
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
def to_expression(val)
|
11
|
+
if val.is_a?(Expression)
|
12
|
+
val
|
13
|
+
else
|
14
|
+
Expression::ValueNode.new(val)
|
15
|
+
end
|
16
|
+
end
|
9
17
|
end
|
10
18
|
end
|
@@ -2,42 +2,43 @@
|
|
2
2
|
|
3
3
|
module MongoQL
|
4
4
|
class StageContext
|
5
|
-
attr_accessor :pipeline
|
5
|
+
attr_accessor :pipeline, :injected_vars
|
6
6
|
|
7
7
|
def initialize
|
8
8
|
@pipeline = []
|
9
|
+
@injected_vars = {}
|
9
10
|
end
|
10
11
|
|
11
12
|
def where(*args)
|
12
|
-
pipeline << Stage::Match.new(*args)
|
13
|
+
pipeline << Stage::Match.new(self, *args)
|
13
14
|
end
|
14
15
|
alias_method :match, :where
|
15
16
|
|
16
17
|
def add_fields(*args)
|
17
|
-
|
18
|
+
pipeline << Stage::AddFields.new(self, *args)
|
18
19
|
end
|
19
20
|
|
20
21
|
def project(*fields)
|
21
|
-
pipeline << Stage::Project.new(*fields)
|
22
|
+
pipeline << Stage::Project.new(self, *fields)
|
22
23
|
end
|
23
24
|
alias_method :select, :project
|
24
25
|
|
25
26
|
def lookup(*args, &block)
|
26
|
-
pipeline << Stage::Lookup.new(*args, &block)
|
27
|
+
pipeline << Stage::Lookup.new(self, *args, &block)
|
27
28
|
end
|
28
29
|
alias_method :join, :lookup
|
29
30
|
|
30
31
|
def group(*args)
|
31
|
-
pipeline << Stage::Group.new(*args)
|
32
|
+
pipeline << Stage::Group.new(self, *args)
|
32
33
|
end
|
33
34
|
|
34
35
|
def unwind(*args)
|
35
|
-
pipeline << Stage::Unwind.new(*args)
|
36
|
+
pipeline << Stage::Unwind.new(self, *args)
|
36
37
|
end
|
37
38
|
alias_method :flatten, :unwind
|
38
39
|
|
39
40
|
def sort(*args)
|
40
|
-
pipeline << Stage::Sort.new(*args)
|
41
|
+
pipeline << Stage::Sort.new(self, *args)
|
41
42
|
end
|
42
43
|
alias_method :sort_by, :sort
|
43
44
|
|
@@ -60,12 +61,7 @@ module MongoQL
|
|
60
61
|
end
|
61
62
|
|
62
63
|
def to_ast
|
63
|
-
|
64
|
-
stages.map do |stage|
|
65
|
-
stage.deep_transform_values do |v|
|
66
|
-
v.is_a?(Expression) ? v.to_ast : v
|
67
|
-
end
|
68
|
-
end
|
64
|
+
pipeline.map(&:to_ast)
|
69
65
|
end
|
70
66
|
|
71
67
|
%w(where match project select sort flatten unwind lookup join).each do |m|
|
@@ -4,7 +4,7 @@ module MongoQL
|
|
4
4
|
module StringOperators
|
5
5
|
def substr(start, length)
|
6
6
|
Expression::MethodCall.new "$substr", self, ast_template: -> (target, **_args) {
|
7
|
-
[target, to_expression(start)
|
7
|
+
[target, to_expression(start), to_expression(length)]
|
8
8
|
}
|
9
9
|
end
|
10
10
|
|
@@ -12,14 +12,14 @@ module MongoQL
|
|
12
12
|
Expression::MethodCall.new "$trim", self, ast_template: -> (target, **_args) {
|
13
13
|
{
|
14
14
|
"input" => target,
|
15
|
-
"chars" => to_expression(chars)
|
15
|
+
"chars" => to_expression(chars)
|
16
16
|
}
|
17
17
|
}
|
18
18
|
end
|
19
19
|
|
20
20
|
def concat(*expressions)
|
21
21
|
Expression::MethodCall.new "$concat", self, ast_template: -> (target, **_args) {
|
22
|
-
[target, *expressions.map { |e| to_expression(e) }
|
22
|
+
[target, *expressions.map { |e| to_expression(e) }]
|
23
23
|
}
|
24
24
|
end
|
25
25
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module MongoQL
|
3
|
+
class Utils
|
4
|
+
class << self
|
5
|
+
def deep_transform_values(value, &block)
|
6
|
+
case value
|
7
|
+
when Hash
|
8
|
+
value.transform_values do |val|
|
9
|
+
MongoQL::Utils.deep_transform_values(val, &block)
|
10
|
+
end
|
11
|
+
when Array
|
12
|
+
value.map do |val|
|
13
|
+
MongoQL::Utils.deep_transform_values(val, &block)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
transformed_value = yield(value)
|
17
|
+
case transformed_value
|
18
|
+
when Hash
|
19
|
+
MongoQL::Utils.deep_transform_values(transformed_value, &block)
|
20
|
+
when Array
|
21
|
+
MongoQL::Utils.deep_transform_values(transformed_value, &block)
|
22
|
+
else
|
23
|
+
transformed_value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/mongo_ql/version.rb
CHANGED
data/lib/mongo_ql.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "active_support/core_ext/hash"
|
3
|
+
require "date"
|
5
4
|
require "logger"
|
6
5
|
module MongoQL
|
7
|
-
class
|
6
|
+
class MongoQLError < RuntimeError; end
|
7
|
+
class InvalidVariableAccess < MongoQLError; end
|
8
|
+
class InvalidValueExpression < MongoQLError; end
|
9
|
+
|
10
|
+
EXPRESSION_TO_AST_MAPPER = proc do |v|
|
11
|
+
case v
|
12
|
+
when Expression, Stage, StageContext
|
13
|
+
v.to_ast
|
14
|
+
else
|
15
|
+
v
|
16
|
+
end
|
17
|
+
end
|
8
18
|
|
9
19
|
def self.compose(*variable_names, &block)
|
10
20
|
block_binding = block.binding
|
@@ -16,17 +26,18 @@ module MongoQL
|
|
16
26
|
|
17
27
|
# Update injected local variables to ValueNode expressions
|
18
28
|
variable_names.each do |name|
|
19
|
-
|
29
|
+
ctx.injected_vars[name] = Expression::ValueNode.new(variables[name])
|
30
|
+
block_binding.local_variable_set(name, ctx.injected_vars[name])
|
20
31
|
end
|
21
32
|
|
22
33
|
ctx.instance_exec(*variables, &block)
|
34
|
+
ctx
|
23
35
|
|
36
|
+
ensure
|
24
37
|
# Restore local variables
|
25
38
|
variable_names.each do |name|
|
26
39
|
block_binding.local_variable_set(name, variables[name])
|
27
40
|
end
|
28
|
-
|
29
|
-
ctx
|
30
41
|
end
|
31
42
|
|
32
43
|
def self.logger
|
@@ -35,6 +46,7 @@ module MongoQL
|
|
35
46
|
end
|
36
47
|
|
37
48
|
require_relative "mongo_ql/version"
|
49
|
+
require_relative "mongo_ql/utils"
|
38
50
|
require_relative "mongo_ql/expression"
|
39
51
|
require_relative "mongo_ql/expression/date_note"
|
40
52
|
require_relative "mongo_ql/expression/field_node"
|
@@ -54,5 +66,6 @@ require_relative "mongo_ql/stage/match"
|
|
54
66
|
require_relative "mongo_ql/stage/group"
|
55
67
|
require_relative "mongo_ql/stage/unwind"
|
56
68
|
require_relative "mongo_ql/stage/sort"
|
69
|
+
require_relative "mongo_ql/stage/add_fields"
|
57
70
|
|
58
71
|
require_relative "mongo_ql/stage_context"
|
data/mongo_ql.gemspec
CHANGED
@@ -19,5 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.
|
23
|
-
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongo_ql
|
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
|
- Xizheng Ding
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-10-
|
11
|
+
date: 2019-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - ">="
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
38
|
- - ">="
|
25
39
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
40
|
+
version: '0'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- dingxizheng@gamil.com
|
@@ -33,11 +47,14 @@ extra_rdoc_files: []
|
|
33
47
|
files:
|
34
48
|
- ".gitignore"
|
35
49
|
- ".rubocop.yml"
|
50
|
+
- ".vscode/launch.json"
|
36
51
|
- LICENSE.txt
|
37
52
|
- README.md
|
53
|
+
- Rakefile
|
38
54
|
- Untitled-1
|
39
55
|
- bin/console
|
40
56
|
- bin/setup
|
57
|
+
- debug.sh
|
41
58
|
- design_specs.md
|
42
59
|
- lib/mongo_ql.rb
|
43
60
|
- lib/mongo_ql/binary_operators.rb
|
@@ -54,6 +71,7 @@ files:
|
|
54
71
|
- lib/mongo_ql/expression/unary.rb
|
55
72
|
- lib/mongo_ql/expression/value_node.rb
|
56
73
|
- lib/mongo_ql/stage.rb
|
74
|
+
- lib/mongo_ql/stage/add_fields.rb
|
57
75
|
- lib/mongo_ql/stage/group.rb
|
58
76
|
- lib/mongo_ql/stage/lookup.rb
|
59
77
|
- lib/mongo_ql/stage/match.rb
|
@@ -63,6 +81,7 @@ files:
|
|
63
81
|
- lib/mongo_ql/stage_context.rb
|
64
82
|
- lib/mongo_ql/string_operators.rb
|
65
83
|
- lib/mongo_ql/unary_operators.rb
|
84
|
+
- lib/mongo_ql/utils.rb
|
66
85
|
- lib/mongo_ql/version.rb
|
67
86
|
- mongo_ql.gemspec
|
68
87
|
homepage: https://github.com/dingxizheng/mongo_ql
|