mongo_ql 0.0.1 → 0.0.2

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: 54ebbab82b00ce95966996491caf76cea95683f8f88be3676f15655c836ffa6b
4
- data.tar.gz: 4e9bd70e840f6f2c118fcba06e8d086ceca67d42bfa8ecd82d3cf8412dd9191e
3
+ metadata.gz: 39d6acd8e471a93e4b6598473110d45935c2f17a1bae9ac2c65435669cd054fb
4
+ data.tar.gz: 0b1efe2e91c015963e9cd0f90b291d14b2b2d0cff55f0c02a3a646327b349ca2
5
5
  SHA512:
6
- metadata.gz: 591efcb215b9769e6be3e4cdbf0d493a6c3800042fec75a649852afd0901b25916ad063516a3f46af05b9a44bdb62eb5cd719f754624d7b15135e85177e3e7d3
7
- data.tar.gz: 586002d366b3d6f264ee8e6621297679febed34b869e6a9c94d4e9698d64a58a07d663b275d313682bef6d7ef16edf1187bd535503312e67acc53abdaea17264
6
+ metadata.gz: ae59e895897c0c23d05b1ad1d2063b0b5f543712876abdd1bb2edd1c75b28dc69686d0dfc1a49c049e61a983920a3e879964a1bf927327a19454377ebcc180c8
7
+ data.tar.gz: 47d606ac87f2c577cd2ea0cb16915f883cafa393b32bd44ccc6a54969fd14404703fff0a5ecfd1928c439d123c58a5dc2966f2553ce73e1ce91f638876a3d5da
data/.gitignore CHANGED
@@ -6,4 +6,5 @@
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
- /tmp/
9
+ /tmp/
10
+ /*.gem
data/bin/console CHANGED
@@ -3,5 +3,15 @@
3
3
  require "bundler/setup"
4
4
  require "./lib/mongo_ql"
5
5
 
6
+ include MongoQL
7
+
8
+ define_singleton_method(:method_missing) do |m, *_args, &_block|
9
+ MongoQL::Expression::FieldNode.new(m)
10
+ end
11
+
12
+ define_singleton_method(:aggregate) do |*variables, &block|
13
+ MongoQL.compose(*variables, &block)
14
+ end
15
+
6
16
  require "irb"
7
17
  IRB.start
data/design_specs.md CHANGED
@@ -16,9 +16,9 @@ Order.where { total >= If(currency == "CAD", 100, 80) }
16
16
  # Aggregation Pipeline DSL
17
17
  ```ruby
18
18
  Order.all.mongo_ql do
19
- join Customer,
20
- :customer_id => _id,
21
- :as => customers
19
+ join Customer,
20
+ on: customer_id == _id.to_id,
21
+ as: customers
22
22
 
23
23
  join Shipping, :as => shippings do
24
24
  match order_id == doc._id,
@@ -26,7 +26,7 @@ Order.all.mongo_ql do
26
26
  end
27
27
 
28
28
  match province == "ON"
29
-
29
+
30
30
  project :_id,
31
31
  :total,
32
32
  :customer => customers.name,
@@ -35,6 +35,11 @@ Order.all.mongo_ql do
35
35
  group customer,
36
36
  :total => total.sum,
37
37
  :total_tax => tax.sum * 5
38
+
39
+ sort_by age.desc
40
+
41
+ page 1
42
+ per 10
38
43
  end
39
44
 
40
45
  # The above aggregation is equivalent to the following mognodb pipeline
@@ -8,11 +8,17 @@ module MongoQL
8
8
  "*": "$multiply",
9
9
  "/": "$divide",
10
10
  ">": "$gt",
11
+ "gt?": "$gt",
11
12
  "<": "$lt",
13
+ "lt?": "$lt",
12
14
  ">=": "$gte",
15
+ "gte?": "$gte",
13
16
  "<=": "$lte",
17
+ "lte?": "$lte",
14
18
  "!=": "$ne",
19
+ "neq?": "$ne",
15
20
  "==": "$eq",
21
+ "eq?": "$eq",
16
22
  "&": "$and",
17
23
  "|": "$or",
18
24
  "%": "$mod",
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Expression::Ascend < Expression
5
+ attr_accessor :field
6
+
7
+ def initialize(field)
8
+ @field = field
9
+ end
10
+
11
+ def to_ast
12
+ { field.to_s => 1 }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Expression::Descend < Expression
5
+ attr_accessor :field
6
+
7
+ def initialize(field)
8
+ @field = field
9
+ end
10
+
11
+ def to_ast
12
+ { field.to_s => -1 }
13
+ end
14
+ end
15
+ end
@@ -19,5 +19,21 @@ module MongoQL
19
19
  def to_ast
20
20
  "$#{field_name}"
21
21
  end
22
+
23
+ def to_s
24
+ field_name.to_s
25
+ end
26
+
27
+ def name
28
+ field_name.to_s
29
+ end
30
+
31
+ def dsc
32
+ Expression::Descend.new(self)
33
+ end
34
+
35
+ def asc
36
+ Expression::Ascend.new(self)
37
+ end
22
38
  end
23
39
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Expression::Projection < Expression
5
+ attr_accessor :field, :expression
6
+
7
+ def initialize(field, expression = 1)
8
+ @expression = case expression
9
+ when 0, 1
10
+ expression
11
+ when Expression::FieldNode
12
+ expression
13
+ else
14
+ raise ArgumentError, "#{expression&.inspect} is not a valid project expression"
15
+ end
16
+
17
+ @field = case field
18
+ when String, Symbol
19
+ Expression::FieldNode.new(field)
20
+ when Expression::FieldNode
21
+ field
22
+ else
23
+ raise ArgumentError, "#{field&.inspect} is not a valid project field"
24
+ end
25
+ end
26
+
27
+ def to_ast
28
+ { field.to_s => expression }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Group < Stage
5
+ EXPRESSION_TO_AST_MAPPER = proc { |v| v.is_a?(Expression) ? v.to_ast : v }
6
+
7
+ attr_accessor :by, :fields
8
+
9
+ def initialize(by, arrow_fields = {}, **fields)
10
+ @by = by
11
+ @fields = fields.transform_keys(&:to_s).merge(arrow_fields.transform_keys(&:to_s))
12
+ end
13
+
14
+ def to_ast
15
+ {
16
+ "$group" => {
17
+ "_id" => by.to_ast,
18
+ }.merge(fields.transform_values(&EXPRESSION_TO_AST_MAPPER))
19
+ }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Lookup < Stage
5
+ class NestedPipelineVars
6
+ attr_accessor :vars
7
+
8
+ def initialize
9
+ @vars = {}
10
+ end
11
+
12
+ def method_missing(m, *args, &block)
13
+ if is_setter?(m)
14
+ set(m, args.first)
15
+ else
16
+ get(m)
17
+ end
18
+ end
19
+
20
+ def get(name)
21
+ vars["var_#{name}"] ||= Expression::FieldNode.new(name)
22
+ Expression::FieldNode.new("$var_#{name}")
23
+ end
24
+
25
+ def set(name, val)
26
+ vars["var_#{name.to_s[0..-2]}"] = val
27
+ end
28
+
29
+ private
30
+ def is_setter?(method_name)
31
+ method_name.to_s.end_with?("=")
32
+ end
33
+ end
34
+
35
+ attr_accessor :from, :condition, :as, :nested_pipeline_block, :let_vars
36
+
37
+ def initialize(from, condition = nil, on: nil, as: nil, &block)
38
+ @from = collection_name(from)
39
+ @as = new_array_name(as)
40
+ @nested_pipeline_block = block
41
+
42
+ if has_nested_pipeline?
43
+ @let_vars = NestedPipelineVars.new
44
+ else
45
+ @condition = condition_ast(condition || on)
46
+ end
47
+ end
48
+
49
+ def to_ast
50
+ lookup_expr = { "from" => from, "as" => as }
51
+ if has_nested_pipeline?
52
+ lookup_expr["pipeline"] = nested_pipeline.to_ast
53
+ lookup_expr["let"] = let_vars.vars
54
+ else
55
+ lookup_expr = lookup_expr.merge(condition)
56
+ end
57
+ { "$lookup" => lookup_expr }
58
+ end
59
+
60
+ private
61
+ def has_nested_pipeline?
62
+ condition.nil? && !nested_pipeline_block.nil?
63
+ end
64
+
65
+ def nested_pipeline
66
+ sub_ctx = StageContext.new
67
+ sub_ctx.instance_exec(let_vars, &nested_pipeline_block)
68
+ sub_ctx
69
+ end
70
+
71
+ def collection_name(from)
72
+ case from
73
+ when String, Symbol
74
+ from
75
+ when Expression::FieldNode
76
+ from.to_s
77
+ else
78
+ if from&.respond_to?(:collection)
79
+ from&.collection&.name
80
+ elsif from&.respond_to?(:name)
81
+ from.name
82
+ else
83
+ raise ArgumentError, "#{from} is not a valid collection"
84
+ end
85
+ end
86
+ end
87
+
88
+ def condition_ast(cond)
89
+ raise ArgumentError, "#{cond.inspect} must be a valid Expression::Binary, example: _id == user_id" unless cond.is_a?(Expression::Binary)
90
+ raise ArgumentError, "#{cond.inspect} must be an 'equal' expression, example: _id == user_id" unless cond.operator == "$eq"
91
+ {
92
+ "localField" => cond&.left_node&.to_s,
93
+ "foreignField" => cond&.right_node&.to_s
94
+ }
95
+ end
96
+
97
+ def new_array_name(as)
98
+ as&.to_s
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Match < Stage
5
+ attr_accessor :conditions, :field_filters
6
+
7
+ def initialize(*conds, **field_filters)
8
+ conds.each do |c|
9
+ raise ArgumentError, "#{c.inspect} is not a MongoQL::Expression" unless c.is_a?(MongoQL::Expression)
10
+ end
11
+ @conditions = conds
12
+ @field_filters = field_filters
13
+ end
14
+
15
+ def to_ast
16
+ conds = {}
17
+ if conditions_ast
18
+ conds["$expr"] = conditions_ast
19
+ end
20
+ { "$match" => conds.merge(field_filters) }
21
+ end
22
+
23
+ private
24
+ def conditions_ast
25
+ if conditions.size > 1
26
+ { "$and" => conditions.map(&:to_ast) }
27
+ elsif conditions.size == 1
28
+ conditions[0].to_ast
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Project < Stage
5
+ attr_accessor :field_projections
6
+
7
+ def initialize(*fields)
8
+ @field_projections = fields.map do |field|
9
+ case field
10
+ when String, Symbol, Expression::FieldNode
11
+ { field.to_s => 1 }
12
+ when Hash
13
+ field.map { |k, v| [k.to_s, to_expression(v).to_ast] }.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
+ { "$project" => field_projections }
22
+ 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
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Sort < Stage
5
+ attr_accessor :fields
6
+
7
+ def initialize(*fields)
8
+ @fields = fields.map do |field|
9
+ case field
10
+ when Expression::FieldNode
11
+ field.asc
12
+ when String, Symbol
13
+ Expression::FieldNode.new(field).asc
14
+ when Expression::Ascend, Expression::Descend
15
+ field
16
+ else
17
+ raise ArgumentError, "#{field.inspect} must be in type [String, Symbol, Expression::FieldNode, Expression::Ascend, Expression::Descend]"
18
+ end
19
+ end
20
+ end
21
+
22
+ def to_ast
23
+ {
24
+ "$sort" => fields.inject({}) { |p, c| p.merge(c.to_ast) }
25
+ }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage::Unwind < Stage
5
+ attr_accessor :path, :allow_null
6
+
7
+ def initialize(path, allow_null: false)
8
+ @path = path.is_a?(Expression) ? path.to_ast : path
9
+ @allow_null = allow_null
10
+ end
11
+
12
+ def to_ast
13
+ {
14
+ "$unwind" => {
15
+ "path" => path,
16
+ "preserveNullAndEmptyArrays" => allow_null
17
+ }
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class Stage
5
+
6
+ def to_ast
7
+ raise NotImplementedError, "stage #{self.class} must implement to_ast"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoQL
4
+ class StageContext
5
+ attr_accessor :pipeline
6
+
7
+ def initialize
8
+ @pipeline = []
9
+ end
10
+
11
+ def where(*args)
12
+ pipeline << Stage::Match.new(*args)
13
+ end
14
+ alias_method :match, :where
15
+
16
+ def add_fields(*args)
17
+ raise NotImplementedError, "add_fields is not implemented"
18
+ end
19
+
20
+ def project(*fields)
21
+ pipeline << Stage::Project.new(*fields)
22
+ end
23
+ alias_method :select, :project
24
+
25
+ def lookup(*args, &block)
26
+ pipeline << Stage::Lookup.new(*args, &block)
27
+ end
28
+ alias_method :join, :lookup
29
+
30
+ def group(*args)
31
+ pipeline << Stage::Group.new(*args)
32
+ end
33
+
34
+ def unwind(*args)
35
+ pipeline << Stage::Unwind.new(*args)
36
+ end
37
+ alias_method :flatten, :unwind
38
+
39
+ def sort(*args)
40
+ pipeline << Stage::Sort.new(*args)
41
+ end
42
+ alias_method :sort_by, :sort
43
+
44
+ def method_missing(m, *args, &block)
45
+ Expression::FieldNode.new(m)
46
+ end
47
+
48
+ def first_of(*field_expressions)
49
+ field_expressions.map do |expr|
50
+ [expr, expr.first]
51
+ end.to_h
52
+ end
53
+
54
+ def f(name)
55
+ Expression::FieldNode.new(name)
56
+ end
57
+
58
+ def v(val)
59
+ Expression::ValueNode.new(val)
60
+ end
61
+
62
+ 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
69
+ end
70
+
71
+ %w(where match project select sort flatten unwind lookup join).each do |m|
72
+ alias_method :"#{m.capitalize}", m
73
+ end
74
+ end
75
+ end
@@ -16,5 +16,11 @@ module MongoQL
16
16
  }
17
17
  }
18
18
  end
19
+
20
+ def concat(*expressions)
21
+ Expression::MethodCall.new "$concat", self, ast_template: -> (target, **_args) {
22
+ [target, *expressions.map { |e| to_expression(e) }.map(&:to_ast)]
23
+ }
24
+ end
19
25
  end
20
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MongoQL
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
data/lib/mongo_ql.rb CHANGED
@@ -1,6 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/hash"
4
+ require "logger"
3
5
  module MongoQL
6
+ class InvalidVariableAccess < StandardError; end
7
+
8
+ def self.compose(*variable_names, &block)
9
+ block_binding = block.binding
10
+ ctx = MongoQL::StageContext.new
11
+
12
+ variables = variable_names.map do |name|
13
+ [name, block_binding.local_variable_get(name)]
14
+ end.to_h
15
+
16
+ # Update injected local variables to ValueNode expressions
17
+ variable_names.each do |name|
18
+ block_binding.local_variable_set(name, Expression::ValueNode.new(variables[name]))
19
+ end
20
+
21
+ ctx.instance_exec(*variables, &block)
22
+
23
+ # Restore local variables
24
+ variable_names.each do |name|
25
+ block_binding.local_variable_set(name, variables[name])
26
+ end
27
+
28
+ ctx
29
+ end
30
+
31
+ def self.logger
32
+ @logger ||= Logger.new($stdout)
33
+ end
4
34
  end
5
35
 
6
36
  require_relative "mongo_ql/version"
@@ -10,4 +40,18 @@ require_relative "mongo_ql/expression/field_node"
10
40
  require_relative "mongo_ql/expression/value_node"
11
41
  require_relative "mongo_ql/expression/method_call"
12
42
  require_relative "mongo_ql/expression/binary"
13
- require_relative "mongo_ql/expression/unary"
43
+ require_relative "mongo_ql/expression/unary"
44
+
45
+ require_relative "mongo_ql/expression/descend"
46
+ require_relative "mongo_ql/expression/ascend"
47
+ require_relative "mongo_ql/expression/projection"
48
+
49
+ require_relative "mongo_ql/stage"
50
+ require_relative "mongo_ql/stage/project"
51
+ require_relative "mongo_ql/stage/lookup"
52
+ require_relative "mongo_ql/stage/match"
53
+ require_relative "mongo_ql/stage/group"
54
+ require_relative "mongo_ql/stage/unwind"
55
+ require_relative "mongo_ql/stage/sort"
56
+
57
+ require_relative "mongo_ql/stage_context"
data/mongo_ql.gemspec CHANGED
@@ -18,4 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.bindir = "exe"
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "activesupport"
21
23
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo_ql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
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-09 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2019-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description:
14
28
  email:
15
29
  - dingxizheng@gamil.com
@@ -30,12 +44,23 @@ files:
30
44
  - lib/mongo_ql/collection_operators.rb
31
45
  - lib/mongo_ql/date_operators.rb
32
46
  - lib/mongo_ql/expression.rb
47
+ - lib/mongo_ql/expression/ascend.rb
33
48
  - lib/mongo_ql/expression/binary.rb
34
49
  - lib/mongo_ql/expression/date_note.rb
50
+ - lib/mongo_ql/expression/descend.rb
35
51
  - lib/mongo_ql/expression/field_node.rb
36
52
  - lib/mongo_ql/expression/method_call.rb
53
+ - lib/mongo_ql/expression/projection.rb
37
54
  - lib/mongo_ql/expression/unary.rb
38
55
  - lib/mongo_ql/expression/value_node.rb
56
+ - lib/mongo_ql/stage.rb
57
+ - lib/mongo_ql/stage/group.rb
58
+ - lib/mongo_ql/stage/lookup.rb
59
+ - lib/mongo_ql/stage/match.rb
60
+ - lib/mongo_ql/stage/project.rb
61
+ - lib/mongo_ql/stage/sort.rb
62
+ - lib/mongo_ql/stage/unwind.rb
63
+ - lib/mongo_ql/stage_context.rb
39
64
  - lib/mongo_ql/string_operators.rb
40
65
  - lib/mongo_ql/unary_operators.rb
41
66
  - lib/mongo_ql/version.rb