mongo_ql 0.0.1 → 0.0.2

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