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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77a5766acd5932125249f1d647015d875df49441a91b96ac6c4391dee41ab00e
4
- data.tar.gz: 4f2706c876c0b933502f5b4a3c20196dda0de92765567cdb6bb5d5f3875ad577
3
+ metadata.gz: e1f6676de369add0c89989cb95013bd170cc5925959f50b74ee1a7824e148b30
4
+ data.tar.gz: 0cd46af4aa9fa2b9a98e10e6ba7532c3aef216e3ca2d3af983b739db2fec2237
5
5
  SHA512:
6
- metadata.gz: 7ad974ec17e7561e368296f6de9088c5966fa0a208cbf2f1db84df85bb3b8d8a4aed917e9549cf2bd02f2470bd3b20d10959ab05dd8c73ba3a7c8f5ab50ad548
7
- data.tar.gz: d37ebf20e12bb1d92c3875a122b7c9e5c88230e1903dec148faafa59fec48257d5dfba76d3377c2b60c43d02f850e6d2823d31271f7beb2b22769ccad7b9f998
6
+ metadata.gz: cc76501d7a5e6e1a13a58fd4164de221f5038afd7006aa7cd8b7b1ec2b4792d278e2797b2d4d0daa117779dd63b4bf34a8c7d5c8b9d110d0ec1c8f3bedde5fd2
7
+ data.tar.gz: 2ec8a62fd293bcd9fc8d266424f18a3206db5be9a8e16ebcb4eb2d9e2679bfbab39e54fd64287440f6235463989a5bae220976dad4ee615b89c5b86d8b29ebb7
@@ -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
- # mongo_ql
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/test_*.rb']
8
+ end
9
+
10
+ task :default => :test
data/debug.sh ADDED
@@ -0,0 +1,2 @@
1
+
2
+ DISABLE_SPRING=1 rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 1234 -- bin/console
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
- Order.all.mongo_ql do
19
- join Customer,
4
+ MongoQL.compose do
5
+ join customers,
20
6
  on: customer_id == _id.to_id,
21
7
  as: customers
22
8
 
23
- join Shipping, :as => shippings do
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
- :total,
32
- :customer => customers.name,
33
- :tax => total * tax_rate
17
+ total,
18
+ customer => customers.name,
19
+ tax => total * tax_rate
34
20
 
35
21
  group customer,
36
- :total => total.sum,
37
- :total_tax => tax.sum * 5
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
- Order.collection.pipeline([
47
- { "$lookup": {
48
- from: "customers",
49
- localField: "$customer_id",
50
- foreignField: "$_id",
51
- as: "customers"
52
- }},
53
- { "$unwind": {
54
- path: "customers"
55
- }},
56
- { "$lookup": {
57
- from: "shippings",
58
- as: "shippings",
59
- let: { doc_id: "$_id" },
60
- pipeline: [{
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
- order_id: { "$eq": "$$dock_id" },
63
- status: :shipped
48
+ "$expr": {
49
+ "$and": [{
50
+ "$eq": ["$order_id", "$$var__id"]
51
+ }, {
52
+ "$eq": ["$status", "shipped"]
53
+ }]
54
+ }
64
55
  }
65
- }]
66
- }},
67
- { "$unwind": {
68
- path: "customers"
69
- }},
70
- { "$match": {
71
- province: { "$eq": "ON" }
72
- }},
73
- { "$project": {
74
- _id: 1,
75
- total: 1,
76
- customer: "$customers.name",
77
- tax: { "$multiply": ["$total", "$tax_rate"] }
78
- }},
79
- { "$group": {
80
- _id: "$customer",
81
- total: { "$sum": "$total" },
82
- total_tax: { "$multiply": [{ "$sum": "$tax" }, 5] }
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" => block.call(Expression::FieldNode.new("$item")).to_ast
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" => block.call(Expression::FieldNode.new("$item")).to_ast
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).to_ast,
49
- "in" => to_expression(block.call(Expression::FieldNode.new("$value"), Expression::FieldNode.new("$this"))).to_ast
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).to_ast, target]
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
@@ -11,7 +11,7 @@ module MongoQL
11
11
  end
12
12
 
13
13
  def to_ast
14
- { operator => [left_node.to_ast, right_node.to_ast] }
14
+ { operator => [left_node, right_node] }
15
15
  end
16
16
  end
17
17
  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.to_ast, **args) }
16
+ { method => ast_template.call(target, **args) }
17
17
  else
18
- { method => target.to_ast }
18
+ { method => target }
19
19
  end
20
20
  end
21
21
  end
@@ -10,7 +10,7 @@ module MongoQL
10
10
  end
11
11
 
12
12
  def to_ast
13
- { operator => right_node.to_ast }
13
+ { operator => right_node }
14
14
  end
15
15
  end
16
16
  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
@@ -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).to_ast]
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
@@ -2,21 +2,22 @@
2
2
 
3
3
  module MongoQL
4
4
  class Stage::Group < Stage
5
- EXPRESSION_TO_AST_MAPPER = proc { |v| v.is_a?(Expression) ? v.to_ast : v }
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.transform_values(&EXPRESSION_TO_AST_MAPPER))
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, :nested_pipeline_block, :let_vars
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.to_ast
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 nested_pipeline
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
@@ -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 conditions_ast
18
- conds["$expr"] = conditions_ast
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 conditions_ast
26
+ def compose_conditions
25
27
  if conditions.size > 1
26
- { "$and" => conditions.map(&:to_ast) }
28
+ { "$and" => conditions }
27
29
  elsif conditions.size == 1
28
- conditions[0].to_ast
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).to_ast] }.to_h
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
@@ -2,9 +2,10 @@
2
2
 
3
3
  module MongoQL
4
4
  class Stage::Sort < Stage
5
- attr_accessor :fields
5
+ attr_accessor :ctx, :fields
6
6
 
7
- def initialize(*fields)
7
+ def initialize(ctx, *fields)
8
+ @ctx = ctx
8
9
  @fields = fields.map do |field|
9
10
  case field
10
11
  when Expression::FieldNode
@@ -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
- @path = path.is_a?(Expression) ? path.to_ast : path
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
@@ -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
- raise NotImplementedError, "add_fields is not implemented"
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
- stages = pipeline.map(&:to_ast)
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).to_ast, to_expression(length).to_ast]
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).to_ast
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) }.map(&:to_ast)]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MongoQL
4
- VERSION = "0.0.4"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/mongo_ql.rb CHANGED
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_support/core_ext/hash"
3
+ require "date"
5
4
  require "logger"
6
5
  module MongoQL
7
- class InvalidVariableAccess < StandardError; end
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
- block_binding.local_variable_set(name, Expression::ValueNode.new(variables[name]))
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.add_runtime_dependency "activesupport", ">= 6.0.0"
23
- end
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
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-17 00:00:00.000000000 Z
11
+ date: 2019-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
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: 6.0.0
20
- type: :runtime
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: 6.0.0
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