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 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