mongoid-report 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NWZhMzhiNGQzZDgxMTdlMjlhZGU2MTUyYTE5YzRjOGJmYTRhYTFjMw==
4
+ NzMwZDY2MzQxNmI5ZmJmYjUxZjhkZTJkM2VlOTg2OGI2YTBmNTBmNA==
5
5
  data.tar.gz: !binary |-
6
- NWEzNzVjYzY1M2IwOTgxZmNmZTc1ZDY3N2VmNjY3NTMwZjFmZWMzYw==
6
+ ZTg4NzljZmFjNDRmOWNkZTExYWY0N2U3MGUxMTIwOTBkNzc0OTkxYw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- Njc2NWE1OWIwYjE5NzJkNmJhOTYxYjM0MjNjMjMyMjkyMjMyZWM5ODVhZjc2
10
- M2YzMDU3MmE3ZjI2MjFiNmVjMDE3MzA2ODIzMjIwOThlOThmOTViYjIxMmU4
11
- MzY1Zjc1NjczYTk3NzViZDQyNGEzNjVhZTI2OTUzMzZlYjM1ZDU=
9
+ NzNiYzA4NmU2YTg1MjQyMDQxNzEwYmIwNzk1ZmRjZGYxOTNkODFjYTI0YWYy
10
+ MjU4MGU2YjQwZWZjOTUxZjdiMWZkMWE4OWRkYzIyZjFkMzEzMDdjYzZkMTJi
11
+ ZGRhNjFmOTc2ZWVjZTM5Y2MwMTJjNWJmYWUxMmYzMDNkNTdlODk=
12
12
  data.tar.gz: !binary |-
13
- YjgyMjcyMWFmZjU2YzVmNGE3MzRjNTQ1ZTZhODQ4M2I3Y2RmOGQ1M2NiZjg4
14
- YmY1Y2UzM2U3ZTI3MDlmODdkMmM3ODAyODVkNzVmY2EyNjA0NzZlNjM1MWVk
15
- ODYwM2Y1OWI5YTlhOTM5ZmY1YmMxOWE2MWFjNGMwNzMzZTE2Y2Q=
13
+ NDMwMzRjMTg0YjE1NTE1ZTcwMmE1OTYxZTUxMTE3NmY1ZmMwNjM0MDAzNTk3
14
+ NjhlMzAxZjFiYTljMmI2MTQ2Y2Q5NWUyODIxNDYzOTczMGI1ZDgyNjAxNjFh
15
+ YmY3YTMzYWIwOTUyNmQ4ZjQyMTZlNjFmNDFiMDdjYzE0OGY3ZjM=
data/Gemfile.lock CHANGED
@@ -1,19 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mongoid-report (0.1.9)
4
+ mongoid-report (0.2.0)
5
5
  mongoid (> 3.0.1)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (3.2.18)
11
- activesupport (= 3.2.18)
12
- builder (~> 3.0.0)
13
- activesupport (3.2.18)
14
- i18n (~> 0.6, >= 0.6.4)
15
- multi_json (~> 1.0)
16
- builder (3.0.4)
10
+ activemodel (4.1.4)
11
+ activesupport (= 4.1.4)
12
+ builder (~> 3.1)
13
+ activesupport (4.1.4)
14
+ i18n (~> 0.6, >= 0.6.9)
15
+ json (~> 1.7, >= 1.7.7)
16
+ minitest (~> 5.1)
17
+ thread_safe (~> 0.1)
18
+ tzinfo (~> 1.1)
19
+ bson (2.3.0)
20
+ builder (3.2.2)
17
21
  celluloid (0.15.2)
18
22
  timers (~> 1.1.0)
19
23
  celluloid-io (0.15.0)
@@ -21,6 +25,7 @@ GEM
21
25
  nio4r (>= 0.5.0)
22
26
  coderay (1.1.0)
23
27
  columnize (0.3.6)
28
+ connection_pool (2.0.0)
24
29
  debugger (1.6.6)
25
30
  columnize (>= 0.3.1)
26
31
  debugger-linecache (~> 1.2.0)
@@ -42,7 +47,8 @@ GEM
42
47
  guard-rspec (4.2.8)
43
48
  guard (~> 2.1)
44
49
  rspec (>= 2.14, < 4.0)
45
- i18n (0.6.9)
50
+ i18n (0.6.11)
51
+ json (1.8.1)
46
52
  listen (2.7.1)
47
53
  celluloid (>= 0.15.2)
48
54
  celluloid-io (>= 0.15.0)
@@ -50,15 +56,19 @@ GEM
50
56
  rb-inotify (>= 0.9)
51
57
  lumberjack (1.0.5)
52
58
  method_source (0.8.2)
53
- mongoid (3.1.6)
54
- activemodel (~> 3.2)
55
- moped (~> 1.4)
56
- origin (~> 1.0)
57
- tzinfo (~> 0.3.29)
58
- moped (1.5.2)
59
- multi_json (1.10.1)
59
+ minitest (5.4.0)
60
+ mongoid (4.0.0)
61
+ activemodel (~> 4.0)
62
+ moped (~> 2.0.0)
63
+ origin (~> 2.1)
64
+ tzinfo (>= 0.3.37)
65
+ moped (2.0.0)
66
+ bson (~> 2.2)
67
+ connection_pool (~> 2.0)
68
+ optionable (~> 0.2.0)
60
69
  nio4r (1.0.0)
61
- origin (1.1.0)
70
+ optionable (0.2.0)
71
+ origin (2.1.1)
62
72
  pry (0.9.12.6)
63
73
  coderay (~> 1.0)
64
74
  method_source (~> 0.8)
@@ -84,8 +94,10 @@ GEM
84
94
  rspec-support (3.0.0)
85
95
  slop (3.5.0)
86
96
  thor (0.19.1)
97
+ thread_safe (0.3.4)
87
98
  timers (1.1.0)
88
- tzinfo (0.3.39)
99
+ tzinfo (1.2.1)
100
+ thread_safe (~> 0.1)
89
101
 
90
102
  PLATFORMS
91
103
  ruby
data/README.md CHANGED
@@ -14,6 +14,7 @@ framework.
14
14
  include Mongoid::Document
15
15
 
16
16
  field :field1, type: Integer, default: 0
17
+ field :field2, type: Integer, default: 0
17
18
 
18
19
  field :day, type: Date
19
20
  end
@@ -21,42 +22,11 @@ framework.
21
22
  class Report1
22
23
  include Mongoid::Report
23
24
 
24
- column :field1, for: Model
25
- end
26
-
27
- class Report2
28
- include Mongoid::Report
29
-
30
- attach_to Model do
31
- column :field1
32
- end
33
- end
34
-
35
- class Report3
36
- include Mongoid::Report
37
-
38
- group_by :day, for: Model
39
-
40
- column :field1, for: Model
41
- end
42
-
43
- class Report4
44
- include Mongoid::Report
45
-
46
- attach_to Model do
47
- group_by :day
48
-
49
- column :field1
50
- end
51
- end
52
-
53
- class Report5
54
- include Mongoid::Report
55
-
56
- attach_to Model, as: 'summary-report' do
57
- group_by :day
58
-
59
- column :field1
25
+ report 'example' do
26
+ attach_to Model do
27
+ group_by :day
28
+ column :field1, collection: Model
29
+ end
60
30
  end
61
31
  end
62
32
  ```
@@ -2,12 +2,21 @@ module Mongoid
2
2
  module Report
3
3
 
4
4
  AttachProxy = Struct.new(:context, :collection, :options) do
5
- attr_reader :attach_name
5
+ attr_reader :report_name
6
6
 
7
7
  def initialize(context, collection, options)
8
8
  # Lets remove as option because of passing to the next blocks options
9
- @attach_name = options.delete(:as) || collection
10
- options = options.merge(attach_name: attach_name, for: collection)
9
+ report_name = options.delete(:as)
10
+
11
+ if collection
12
+ report_name ||= Collections.name(collection)
13
+ end
14
+
15
+ options.merge!(
16
+ report_name: report_name,
17
+ collection: collection,
18
+ )
19
+
11
20
  super(context, collection, options)
12
21
  end
13
22
 
@@ -35,10 +44,22 @@ module Mongoid
35
44
  context.group_by(*fields, proxy_options)
36
45
  end
37
46
 
38
- def filter(*fields)
47
+ def match(*fields)
48
+ proxy_options = fields.extract_options!
49
+ proxy_options.merge!(options)
50
+ context.match(*fields, proxy_options)
51
+ end
52
+
53
+ def query(*fields)
54
+ proxy_options = fields.extract_options!
55
+ proxy_options.merge!(options)
56
+ context.query(*fields, proxy_options)
57
+ end
58
+
59
+ def batches(*fields)
39
60
  proxy_options = fields.extract_options!
40
61
  proxy_options.merge!(options)
41
- context.filter(*fields, proxy_options)
62
+ context.batches(*fields, proxy_options)
42
63
  end
43
64
  end
44
65
 
@@ -0,0 +1,41 @@
1
+ module Mongoid
2
+ module Report
3
+
4
+ # Split the queries into threads.
5
+ Batches = Struct.new(:settings, :conditions) do
6
+ DEFAULT_THREAD_POOL_SIZE = 5
7
+
8
+ def initialize(settings = {}, conditions = {})
9
+ if settings.nil? || settings.empty?
10
+ settings = { 'pool_size' => DEFAULT_THREAD_POOL_SIZE }
11
+ end
12
+
13
+ super(settings, conditions)
14
+ end
15
+
16
+ def field
17
+ conditions.keys[0]
18
+ end
19
+
20
+ def range
21
+ conditions.values[0]
22
+ end
23
+
24
+ def map
25
+ range.each_slice(size.ceil).map do |r|
26
+ yield r
27
+ end
28
+ end
29
+
30
+ def size
31
+ range.count.to_f / settings['pool_size'].to_f
32
+ end
33
+
34
+ def present?
35
+ settings['pool_size'].present? &&
36
+ conditions.present?
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -3,7 +3,7 @@ require 'delegate'
3
3
  module Mongoid
4
4
  module Report
5
5
 
6
- class Collection < SimpleDelegator
6
+ class Collection
7
7
  def initialize(context, rows, fields, columns, mapping)
8
8
  @context = context
9
9
  @rows = rows
@@ -15,7 +15,7 @@ module Mongoid
15
15
 
16
16
  class Rows < SimpleDelegator ; end
17
17
 
18
- attr_reader :rows
18
+ attr_reader :context, :rows
19
19
 
20
20
  def headers
21
21
  @fields
@@ -29,13 +29,14 @@ module Mongoid
29
29
  # all summaried mongo columns and then apply dynamic columns
30
30
  # calculations.
31
31
  next if @columns.has_key?(field)
32
+ next unless row[field].is_a?(Fixnum) || row[field].is_a?(Float)
32
33
  summary[field] += row[field]
33
34
  end
34
35
 
35
36
  # Apply dynamic columns for summarized row
36
37
  @columns.each do |name, function|
37
38
  next unless @fields.include?(name)
38
- summary[name] = function.call(@context, row, { mapping: @mapping, summary: true })
39
+ summary[name] = function.call(@context, summary, { mapping: @mapping, summary: true })
39
40
  end
40
41
 
41
42
  summary
@@ -0,0 +1,16 @@
1
+ module Mongoid
2
+ module Report
3
+
4
+ class Collections
5
+ def self.get(collection_name)
6
+ Mongoid.session(:default)[collection_name]
7
+ end
8
+
9
+ def self.name(model)
10
+ model && model.respond_to?(:collection) ?
11
+ model.collection.name : model
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Mongoid
2
+ module Report
3
+
4
+ class Input
5
+ attr_accessor :collection_name
6
+
7
+ def present?
8
+ collection_name.present?
9
+ end
10
+
11
+ def collection
12
+ @collection ||= Mongoid::Report::Collections.get(collection_name)
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module Mongoid
2
+ module Report
3
+
4
+ # We are using this class to combine results by group by fields.
5
+ Merger = Struct.new(:groups) do
6
+ def do(rows)
7
+ # Merge by groups.
8
+ rows
9
+ .group_by { |row| groups.map { |group| row[group] }.join('-') }
10
+ .values
11
+ .map { |array_row| combine(array_row) }
12
+ end
13
+
14
+ private
15
+
16
+ def combine(rows)
17
+ rows.inject(Hash.new {|h,k| h[k] = 0}) do |row, lines|
18
+ lines.each do |key, value|
19
+ next row[key] = value if groups.include?(key)
20
+ row[key] += value
21
+ end
22
+
23
+ row
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Mongoid
2
+ module Report
3
+
4
+ class Output
5
+ attr_accessor :collection_name, :options
6
+
7
+ def do(rows)
8
+ drop()
9
+ collection.insert(rows)
10
+ end
11
+
12
+ def present?
13
+ collection_name.present?
14
+ end
15
+
16
+ def drop
17
+ return collection.drop() unless options[:drop].present?
18
+
19
+ # We will use custom way for dropping the collection or removing the
20
+ # records partially
21
+ collection.find(options[:drop]).remove_all()
22
+ end
23
+
24
+ private
25
+
26
+ def collection
27
+ @collection ||= Collections.get(collection_name)
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -1,7 +1,7 @@
1
1
  module Mongoid
2
2
  module Report
3
3
 
4
- QueriesBuilder = Struct.new(:settings) do
4
+ QueriesBuilder = Struct.new(:configuration) do
5
5
  def do
6
6
  [].tap do |queries|
7
7
  queries.concat([{ '$project' => project_query }])
@@ -13,26 +13,22 @@ module Mongoid
13
13
  private
14
14
 
15
15
  def groups
16
- @group_by ||= settings.fetch(:group_by, [])
16
+ configuration[:group_by]
17
17
  end
18
18
 
19
19
  def fields
20
- @fields ||= settings[:fields].select do |field, _|
21
- !settings[:columns].include?(field.to_sym)
22
- end
23
- end
24
-
25
- def in_fields
26
- @in_fields ||= fields.keys
27
- end
20
+ @fields ||= begin
21
+ columns = configuration[:columns].keys
28
22
 
29
- def output_fields
30
- @output_fields ||= fields.values
23
+ configuration[:fields].select do |field|
24
+ !columns.include?(field.to_sym)
25
+ end
26
+ end
31
27
  end
32
28
 
33
29
  def all_fields
34
30
  [:_id]
35
- .concat(in_fields)
31
+ .concat(fields)
36
32
  .concat(groups)
37
33
  end
38
34
 
@@ -52,7 +48,8 @@ module Mongoid
52
48
  hash.merge!(group => GROUP_TEMPLATE % group)
53
49
  end
54
50
 
55
- in_fields.inject(query) do |hash, field|
51
+ fields.inject(query) do |hash, field|
52
+ next hash if groups.include?(field)
56
53
  hash.merge!(field => { '$sum' => GROUP_TEMPLATE % field })
57
54
  end
58
55
  end
@@ -71,8 +68,9 @@ module Mongoid
71
68
  end
72
69
  end
73
70
 
74
- fields.inject(query) do |hash, (field, name)|
75
- hash.merge!(name => "$#{field}")
71
+ fields.inject(query) do |hash, field|
72
+ next hash if groups.include?(field)
73
+ hash.merge!(field => "$#{field}")
76
74
  end
77
75
  end
78
76
  end
@@ -1,13 +1,25 @@
1
+ require 'securerandom'
2
+
1
3
  module Mongoid
2
4
  module Report
3
5
 
4
6
  ReportProxy = Struct.new(:context, :name) do
5
- def attach_to(model, options = {}, &block)
6
- as = options.fetch(:as) { model.collection.name }
7
+ def attach_proxy
8
+ @attach_proxy ||= begin
9
+ AttachProxy.new(context, nil, report_module: name)
10
+ end
11
+ end
12
+ delegate :column, :columns, :mapping, :group_by, :match,
13
+ :batches, :query, to: :attach_proxy
14
+
15
+ def attach_to(*fields, &block)
16
+ options = fields.extract_options!
17
+ model = fields[0]
7
18
 
8
- options.merge!(as: "#{name}-#{as}")
19
+ report_name = options.delete(:as) || Collections.name(model)
20
+ options.merge!(report_module: name, as: report_name)
9
21
 
10
- context.attach_to(model, options, &block)
22
+ context.attach_to(*fields, options, &block)
11
23
  end
12
24
  end
13
25
 
@@ -1,7 +1,9 @@
1
+ require 'set'
2
+
1
3
  module Mongoid
2
4
  module Report
3
5
 
4
- Scope = Struct.new(:context, :report_name) do
6
+ Scope = Struct.new(:context, :report_module, :report_name) do
5
7
  def query(conditions = {})
6
8
  queries.concat([conditions]) unless conditions.empty?
7
9
  self
@@ -11,41 +13,112 @@ module Mongoid
11
13
  def yield
12
14
  return self if @yielded
13
15
 
14
- queries.concat(context.queries(report_name))
16
+ queries.concat(context.queries(report_module, report_name))
15
17
  @yielded = true
16
18
 
17
19
  self
18
20
  end
19
21
 
22
+ def out(collection_name, options = {})
23
+ output.collection_name = collection_name
24
+ output.options = options
25
+ self
26
+ end
27
+
28
+ def in(collection_name)
29
+ input.collection_name = collection_name
30
+ self
31
+ end
32
+
33
+ def all_in_batches(aggregation_queries)
34
+ # Lets assume we have only one field for making splits for the
35
+ # aggregation queries.
36
+ rows = []
37
+
38
+ threads = batches.map do |r|
39
+ # For now we are supporting only data fields for splitting up the
40
+ # queries.
41
+ range_match = r.map { |time| time.to_date.mongoize }
42
+
43
+ Thread.new do
44
+ q =
45
+ ['$match' => { batches.field => { '$gte' => range_match.first, '$lte' => range_match.last } }] +
46
+ aggregation_queries
47
+
48
+ # if groups == [batch.field]
49
+ rows.concat(Array(collection.aggregate(q)))
50
+ end
51
+ end
52
+ threads.map(&:join)
53
+
54
+ merger = Mongoid::Report::Merger.new(groups)
55
+ merger.do(rows)
56
+ end
57
+
58
+ def all_inline(aggregation_queries)
59
+ Array(collection.aggregate(aggregation_queries))
60
+ end
61
+
20
62
  def all
21
63
  self.yield unless yielded?
22
64
 
23
65
  aggregation_queries = compile_queries
24
- rows = klass.collection.aggregate(aggregation_queries)
66
+
67
+ rows = if batches.present?
68
+ all_in_batches(aggregation_queries)
69
+ else
70
+ all_inline(aggregation_queries)
71
+ end
72
+
73
+ # in case if we want to store rows to collection
74
+ if output.present?
75
+ output.do(rows)
76
+ end
25
77
 
26
78
  Collection.new(context, rows, fields, columns, mapping)
27
79
  end
28
80
 
81
+ def in_batches(conditions)
82
+ batches.conditions = conditions
83
+ self
84
+ end
85
+
29
86
  private
30
87
 
31
88
  def compile_queries
32
- compiled = queries.map do |query|
33
- next query unless query.has_key?("$match")
89
+ compiled = Set.new
90
+
91
+ queries.each do |query|
92
+ next compiled << query if query.has_key?("$project") || query.has_key?('$group')
34
93
 
35
94
  query.deep_dup.tap do |new_query|
36
95
  new_query.each do |function_name, values|
37
- values.each do |name, value|
38
- if value.respond_to?(:call)
39
- value = value.call(context)
96
+
97
+ if values.respond_to?(:call)
98
+ new_query[function_name] = values.call(context)
99
+ else
100
+ values.each do |name, value|
101
+ if value.respond_to?(:call)
102
+ value = value.call(context)
103
+ end
104
+
105
+ unless value.present?
106
+ # In case we don't have value for applying match, lets skip
107
+ # this type of the queries.
108
+ new_query.delete(function_name)
109
+ else
110
+ new_query[function_name][name] = value
111
+ end
40
112
  end
41
113
 
42
- new_query[function_name][name] = value
43
- end
44
- end
114
+ end # values.is_a?(Proc)
115
+ end # new_query.each
116
+
117
+ compiled << new_query if new_query.present?
45
118
  end
46
119
  end
47
120
 
48
- compiled
121
+ compiled.to_a
49
122
  end
50
123
 
51
124
  def yielded?
@@ -56,22 +129,55 @@ module Mongoid
56
129
  @queries ||= []
57
130
  end
58
131
 
59
- def klass
60
- context.report_module_settings[report_name][:for]
132
+ # Different usage for this method:
133
+ # - attach_to method contains collection name as first argument
134
+ # - attach_to method contains mongoid model
135
+ # - aggregate_for method contains attach_to proc option for calculating
136
+ # collection name.
137
+ def collection
138
+ @collection ||= begin
139
+ # In case if we are using dynamic collection name calculated by
140
+ # passing attach_to proc to the aggregate method.
141
+ if input.present?
142
+ # Using default session to mongodb we can automatically provide
143
+ # access to collection.
144
+ input.collection
145
+ else
146
+ klass = context.report_module_settings[report_module][:reports][report_name][:collection]
147
+ Collections.get(klass)
148
+ end
149
+ end
150
+ end
151
+
152
+ def batches
153
+ @batches ||= Mongoid::Report::Batches.new(
154
+ context.batches(report_module, report_name))
155
+ end
156
+
157
+ def output
158
+ @output ||= Mongoid::Report::Output.new
159
+ end
160
+
161
+ def input
162
+ @input ||= Mongoid::Report::Input.new
163
+ end
164
+
165
+ def groups
166
+ @groups ||= context.groups(report_module, report_name)
61
167
  end
62
168
 
63
169
  def fields
64
170
  # We need to use here only output field names it could be different
65
171
  # than defined colunms, Example: field1: 'report-field-name'
66
- context.report_module_settings[report_name][:fields].values
172
+ context.fields(report_module, report_name)
67
173
  end
68
174
 
69
175
  def columns
70
- context.report_module_settings[report_name][:columns]
176
+ context.columns(report_module, report_name)
71
177
  end
72
178
 
73
179
  def mapping
74
- context.report_module_settings[report_name][:mapping]
180
+ context.mapping(report_module, report_name)
75
181
  end
76
182
  end
77
183