mongoid-report 0.1.9 → 0.2.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,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