mongo_ql 0.0.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|