activecube 0.1.1 → 0.1.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: baf4f5a3146bb1c03244f62328b0af901ae7882e41ff4234a86e1deb6cd881a9
4
- data.tar.gz: 44ca0d2eefbf2b4cc38e95aa9ab2b352607b4817b5c5aaebce8b2a04022037f6
3
+ metadata.gz: fc8d353c9c9d670cda8c4f9de4a3e59994c4f8e6ebd2d25551a5bf712f53db00
4
+ data.tar.gz: 55ac707acc8dd783318739e8f4bb424454da0c597a54ab89b09c76a16982ac23
5
5
  SHA512:
6
- metadata.gz: ac3b3dd34000d54054eba5a0fc7b6aa9803f7643f4f1cc67c88fc603c9e34d07a7ae7ef2eed755efca7bdcd60f532c25b67962b45f20550df98bcb2fc2e13a19
7
- data.tar.gz: 408881f9f8f5c3c3b2af544f320846f8f9765cf3f9673e196c9d2f32555901c221539786eb4c3fa487a082f1d62bf2939442f836fe73cdf6e559bea4ba5f8945
6
+ metadata.gz: 7652d255be72680539226092333c4c1fa081a0416b4ef50a13de17d40385c45628be5e761d1191b9e7e5ab1b55c26ca613012d9e601fda4c44984249bd7053e2
7
+ data.tar.gz: b20ad10bbec9f17cc95a879f84690739fcb3109e451797e954efd720a0de3fbaeac0a38e7b4a0ab1e647d2f3666d0b4b3de7c93d6bd3441897d3a72ab9db03f2
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- activecube (0.1.0)
12
+ activecube (0.1.2)
13
13
  activerecord (>= 5.2)
14
14
 
15
15
  GEM
data/README.md CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  Activecube is the library to make multi-dimensional queries to data warehouse, such as:
4
4
 
5
- ```sql
6
-
5
+ ```ruby
7
6
  Cube.slice(
8
7
  date: cube.dimensions[:date][:date].format('%Y-%m'),
9
8
  currency: cube.dimensions[:currency][:symbol]
@@ -132,8 +131,10 @@ The methods used to contruct the query:
132
131
 
133
132
  - **slice** defines which dimensions slices the results
134
133
  - **measure** defines what to measure
135
- - **where** defines which selectors to apply
136
- - **desc, asc, take, limit** are for ordering and limiting result set
134
+ - **when** defines which selectors to apply
135
+ - **desc, asc, take, skip** are for ordering and limiting result set
136
+
137
+ (take and skip have aliases: offset and limit).
137
138
 
138
139
  After the query contructed, the following methods can be applied:
139
140
 
@@ -141,11 +142,13 @@ After the query contructed, the following methods can be applied:
141
142
  - **to_query** to generate Arel query
142
143
  - **query** to execute query and return ResultSet
143
144
 
144
- Note, that you can control the connection used to construct and execute query by
145
+ ### Managing Connections
146
+
147
+
148
+ You can control the connection used to construct and execute query by
145
149
  ActiveRecord standard API:
146
150
 
147
151
  ```ruby
148
-
149
152
  ApplicationRecord.connected_to(database: :data_warehouse) do
150
153
  cube = My::TransfersCube
151
154
  cube.slice(
@@ -158,6 +161,42 @@ ApplicationRecord.connected_to(database: :data_warehouse) do
158
161
  will query using data_warehouse configuraton.
159
162
 
160
163
 
164
+ Alternatively you can use the method provided by activecube. It will
165
+ make the connection for the model or abstract class, which is super class for your models:
166
+
167
+ ```ruby
168
+ My::TransfersCube.connected_to(database: :data_warehouse) do |cube|
169
+ cube.slice(
170
+ date: cube.dimensions[:date][:date].format('%Y-%m'),
171
+ currency: cube.dimensions[:currency][:symbol]
172
+ ).measure(:count).query
173
+ end
174
+ ```
175
+
176
+ ## How it works
177
+
178
+ When you construct and execute cube query with any outcome ( sql, Arel query or ResultSet),
179
+ the same sequence of operations happen:
180
+
181
+ 1) Cube is collecting the query into a set of objects from the chain method call;
182
+ 2) Query is matched against the physical tables, the tables are selected that can serve the query or its part. For example, one table can provide one set of metrics, and the other can provide remaining;
183
+ 3) If possible, the variant is selected from all possible options, which uses indexes with the most cardinality
184
+ 4) Query is constructed using Arel SQL engine ( included in ActiveRecord ) using selected tables, and possibly joins
185
+ 5) If requested, the query is converted to sql ( using Arel visitor ) or executed with database connection
186
+
187
+ ## Optimization
188
+
189
+ The optimization on step #3 try to minimize the total cost of execution:
190
+
191
+ ![Formula min max](https://latex.codecogs.com/png.latex?min(\sum_{tables}max_{metrics}(cost))))
192
+
193
+ where
194
+
195
+ ![Formula cost](https://latex.codecogs.com/png.latex?\inline&space;cost(metric,table)&space;=&space;1&space;/&space;(1&space;+&space;cardinality(metric,&space;table)))
196
+
197
+ Optimization is done using the algorithm, which checks possible combinations of metrics and tables.
198
+
199
+
161
200
  ## Development
162
201
 
163
202
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/activecube.rb CHANGED
@@ -6,8 +6,7 @@ require 'activecube/dimension'
6
6
  require 'activecube/metric'
7
7
  require 'activecube/selector'
8
8
 
9
- require 'activecube/common/count'
10
- require 'activecube/common/sum'
9
+ require 'activecube/common/metrics'
11
10
 
12
11
  require 'active_record'
13
12
 
@@ -0,0 +1,25 @@
1
+ module Activecube::Common
2
+
3
+ module Metrics
4
+
5
+ METHODS = [:count, :minimum,:maximum,:average,:sum,:uniqueExact,:unique,:median,:any,:anyLast]
6
+
7
+ METHODS.each do |fname|
8
+
9
+ if fname==:count
10
+ define_method fname do |model, arel_table, measure, cube_query|
11
+ measure.selectors.empty? ? Arel.star.count : Arel.star.countIf(measure.condition_query model, arel_table, cube_query)
12
+ end
13
+ else
14
+ define_method fname do |model, arel_table, measure, cube_query|
15
+ column = arel_table[self.class.column_name.to_sym]
16
+ measure.selectors.empty? ? column.send(fname) : column.send(fname.to_s+'If', measure.condition_query(model, arel_table, cube_query))
17
+ end
18
+ end
19
+
20
+
21
+ end
22
+
23
+
24
+ end
25
+ end
@@ -1,10 +1,12 @@
1
1
  require 'activecube/cube_definition'
2
2
  require 'activecube/field'
3
+ require 'activecube/modifier'
3
4
 
4
5
  module Activecube
5
- module DimensionDefinitionMethods
6
6
 
7
- attr_reader :column_names, :identity, :fields
7
+ module DefinitionMethods
8
+
9
+ attr_reader :column_names
8
10
 
9
11
  def column_name
10
12
  raise "Not defined column for a metric #{self.name}" if column_names.empty?
@@ -22,14 +24,41 @@ module Activecube
22
24
  array.concat data
23
25
  end
24
26
 
25
- def field *args
26
- (@fields ||= {} )[args.first.to_sym] = Field.new( *args)
27
- end
27
+
28
+ end
29
+
30
+ module DimensionDefinitionMethods
31
+
32
+ include DefinitionMethods
33
+
34
+ attr_reader :identity, :fields
35
+
36
+ private
28
37
 
29
38
  def identity_column *args
30
39
  raise "Identity already defined as #{identity} for #{self.name}" if @identity
31
40
  @identity = args.first
32
41
  end
33
42
 
43
+ def field *args
44
+ (@fields ||= {} )[args.first.to_sym] = Field.new( *args)
45
+ end
46
+
47
+ end
48
+
49
+
50
+ module MetricDefinitionMethods
51
+
52
+ include DefinitionMethods
53
+
54
+ attr_reader :modifiers
55
+
56
+ private
57
+
58
+ def modifier *args
59
+ (@modifiers ||= {} )[args.first.to_sym] = Modifier.new( *args)
60
+ end
61
+
34
62
  end
63
+
35
64
  end
@@ -1,4 +1,4 @@
1
- require 'activecube/dimension_definition_methods'
1
+ require 'activecube/definition_methods'
2
2
 
3
3
  module Activecube
4
4
  class Dimension
@@ -1,7 +1,7 @@
1
- require 'activecube/dimension_definition_methods'
1
+ require 'activecube/definition_methods'
2
2
 
3
3
  module Activecube
4
4
  class Metric
5
- extend DimensionDefinitionMethods
5
+ extend MetricDefinitionMethods
6
6
  end
7
7
  end
@@ -0,0 +1,12 @@
1
+ module Activecube
2
+ class Modifier
3
+
4
+ attr_reader :name, :definition
5
+ def initialize *args
6
+ @name = args.first
7
+ @definition = args.second
8
+ end
9
+
10
+
11
+ end
12
+ end
@@ -25,7 +25,7 @@ module Activecube::Processor
25
25
  query = table
26
26
 
27
27
  (cube_query.slices + cube_query.measures + cube_query.selectors + cube_query.options).each do |s|
28
- query = s.append_query cube_query, table, query
28
+ query = s.append_query model, cube_query, table, query
29
29
  end
30
30
 
31
31
  query
@@ -42,7 +42,7 @@ module Activecube::Processor
42
42
  using(*dimension_names)
43
43
 
44
44
  cube_query.options.each do |option|
45
- query = option.append_query cube_query, outer_table, query
45
+ query = option.append_query model, cube_query, outer_table, query
46
46
  end
47
47
 
48
48
 
@@ -14,7 +14,7 @@ module Activecube::Query
14
14
 
15
15
  include ChainAppender
16
16
 
17
- attr_reader :cube, :slices, :measures, :selectors, :orderings, :options
17
+ attr_reader :cube, :slices, :measures, :selectors, :options
18
18
  def initialize cube, slices = [], measures = [], selectors = [], options = []
19
19
  @cube = cube
20
20
  @slices = slices
@@ -31,7 +31,7 @@ module Activecube::Query
31
31
  append *args, @measures, Measure, cube.metrics
32
32
  end
33
33
 
34
- def select *args
34
+ def when *args
35
35
  append *args, @selectors, Selector, cube.selectors
36
36
  end
37
37
 
@@ -63,6 +63,9 @@ module Activecube::Query
63
63
  self
64
64
  end
65
65
 
66
+ alias_method :limit, :take
67
+ alias_method :offset, :skip
68
+
66
69
  def query
67
70
  composer = Activecube::Processor::Composer.new(self)
68
71
  sql = composer.build_query.to_sql
@@ -101,7 +104,7 @@ module Activecube::Query
101
104
  reduced_selectors = self.selectors
102
105
  else
103
106
  reduced_measures = other_measures.collect{|m|
104
- Measure.new m.cube, m.key, m.definition, (m.selectors - common_selectors)
107
+ Measure.new m.cube, m.key, m.definition, (m.selectors - common_selectors), m.modifications
105
108
  }
106
109
  reduced_selectors = self.selectors + common_selectors
107
110
  end
@@ -119,5 +122,9 @@ module Activecube::Query
119
122
  slices.map{|s| s.dimension_class.identity || s.key }.uniq
120
123
  end
121
124
 
125
+ def orderings
126
+ options.select{|s| s.kind_of? Ordering}
127
+ end
128
+
122
129
  end
123
130
  end
@@ -18,5 +18,9 @@ module Activecube::Query
18
18
  self.class.new cube, new_key, definition
19
19
  end
20
20
 
21
+ def to_s
22
+ "#{definition.class.name}(#{key})"
23
+ end
24
+
21
25
  end
22
26
  end
@@ -8,7 +8,7 @@ module Activecube
8
8
  @option = option
9
9
  end
10
10
 
11
- def append_query _cube_query, _table, query
11
+ def append_query _model, _cube_query, _table, query
12
12
  query.send(option,argument)
13
13
  end
14
14
 
@@ -1,11 +1,16 @@
1
+ require 'activecube/modifier'
2
+ require 'activecube/query/modification'
3
+
1
4
  module Activecube::Query
2
5
  class Measure < Item
6
+ attr_reader :selectors, :modifications
3
7
 
4
- attr_reader :selectors
5
-
6
- def initialize cube, key, definition, selectors = []
8
+ def initialize cube, key, definition, selectors = [], modifications = []
7
9
  super cube, key, definition
8
10
  @selectors = selectors
11
+ @modifications = modifications
12
+
13
+ modifier_methods! if definition.class.modifiers
9
14
  end
10
15
 
11
16
  def required_column_names
@@ -17,24 +22,44 @@ module Activecube::Query
17
22
  end
18
23
 
19
24
  def alias! new_key
20
- self.class.new cube, new_key, definition, selectors
25
+ self.class.new cube, new_key, definition, selectors, modifications
21
26
  end
22
27
 
23
- def condition_query arel_table, cube_query
28
+ def condition_query model, arel_table, cube_query
24
29
  condition = nil
25
30
  selectors.each do |selector|
26
31
  condition = condition ?
27
- condition.and(selector.expression(arel_table, cube_query)) :
28
- selector.expression(arel_table, cube_query)
32
+ condition.and(selector.expression(model, arel_table, cube_query)) :
33
+ selector.expression(model, arel_table, cube_query)
29
34
  end
30
35
  condition
31
36
  end
32
37
 
33
- def append_query cube_query, table, query
38
+ def append_query model, cube_query, table, query
34
39
  attr_alias = "`#{key.to_s}`"
35
- expr = definition.expression table, self, cube_query
40
+ expr = definition.expression model, table, self, cube_query
36
41
  query.project expr.as(attr_alias)
37
42
  end
38
43
 
44
+ def to_s
45
+ "Metric #{super}"
46
+ end
47
+
48
+ def modifier name
49
+ ms = modifications.select{|m| m.modifier.name==name}
50
+ raise "Found multiple (#{ms.count}) definitions for #{name} in #{self}" if ms.count>1
51
+ ms.first
52
+ end
53
+
54
+ private
55
+
56
+ def modifier_methods!
57
+ definition.class.modifiers.each_pair do |key, modifier|
58
+ define_singleton_method key do |*args|
59
+ (@modifications ||= []) << Modification.new(modifier, *args)
60
+ self
61
+ end
62
+ end
63
+ end
39
64
  end
40
65
  end
@@ -0,0 +1,14 @@
1
+ require 'activecube/modifier'
2
+ module Activecube
3
+ module Query
4
+ class Modification
5
+
6
+ attr_reader :modifier, :args
7
+ def initialize modifier, *args
8
+ @modifier = modifier
9
+ @args = args
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -9,10 +9,10 @@ module Activecube
9
9
  @selectors = selectors
10
10
  end
11
11
 
12
- def append_query cube_query, table, query
12
+ def append_query model, cube_query, table, query
13
13
  expr = nil
14
14
  selectors.each do |s|
15
- expr = expr ? expr.or(s.expression table, cube_query) : s.expression(table, cube_query)
15
+ expr = expr ? expr.or(s.expression model, table, cube_query) : s.expression(model, table, cube_query)
16
16
  end
17
17
  query.where(expr)
18
18
  end
@@ -8,7 +8,7 @@ module Activecube
8
8
  @direction = direction
9
9
  end
10
10
 
11
- def append_query _cube_query, _table, query
11
+ def append_query _model, _cube_query, _table, query
12
12
  query.order(::Arel.sql(argument.to_s).send(direction))
13
13
  end
14
14
 
@@ -1,7 +1,7 @@
1
1
  module Activecube::Query
2
2
  class Selector < Item
3
3
 
4
- OPERATORS = ['eq','ne','gt','lt','ge','le','in','not_in']
4
+ OPERATORS = ['eq','ne','gt','lt','gteq','lteq','in','not_in','between']
5
5
  ARRAY_OPERATORS = ['in','not_in']
6
6
 
7
7
  class Operator
@@ -13,7 +13,7 @@ module Activecube::Query
13
13
  @argument = argument
14
14
  end
15
15
 
16
- def expression left, right
16
+ def expression _model, left, right
17
17
  left.send(operation, right)
18
18
  end
19
19
 
@@ -33,6 +33,7 @@ module Activecube::Query
33
33
 
34
34
  end
35
35
 
36
+
36
37
  attr_reader :operator
37
38
  def initialize cube, key, definition, operator = nil
38
39
  super cube, key, definition
@@ -41,26 +42,42 @@ module Activecube::Query
41
42
 
42
43
  OPERATORS.each do |method|
43
44
  define_method(method) do |*args|
45
+ raise ArgumentError, "Selector for #{method} already set" if operator
44
46
  if ARRAY_OPERATORS.include? method
45
47
  @operator = Operator.new(method, args)
48
+ elsif method=='between'
49
+ if args.kind_of?(Range)
50
+ @operator = Operator.new(method, args)
51
+ elsif args.kind_of?(Array) && (arg = args.flatten).count==2
52
+ @operator = Operator.new(method, arg[0]..arg[1])
53
+ else
54
+ raise ArgumentError, "Unexpected size of arguments for #{method}, must be Range or Array of 2"
55
+ end
46
56
  else
47
- raise ArgumentError, "Unexpected size of arguments" unless args.size==1
57
+ raise ArgumentError, "Unexpected size of arguments for #{method}" unless args.size==1
48
58
  @operator = Operator.new(method, args.first)
49
59
  end
50
60
  self
51
61
  end
52
62
  end
53
63
 
64
+ alias_method :since, :gteq
65
+ alias_method :till, :lteq
66
+ alias_method :is, :eq
67
+ alias_method :not, :ne
68
+ alias_method :after, :gt
69
+ alias_method :before, :lt
70
+
54
71
  def alias! new_key
55
72
  self.class.new cube, new_key, definition, operator
56
73
  end
57
74
 
58
- def append_query cube_query, table, query
59
- query.where(expression table, cube_query)
75
+ def append_query model, cube_query, table, query
76
+ query.where(expression model, table, cube_query)
60
77
  end
61
78
 
62
- def expression arel_table, cube_query
63
- definition.expression arel_table, self, cube_query
79
+ def expression model, arel_table, cube_query
80
+ definition.expression model, arel_table, self, cube_query
64
81
  end
65
82
 
66
83
  def eql?(other)
@@ -78,5 +95,9 @@ module Activecube::Query
78
95
  self.definition.class.hash + self.operator.hash
79
96
  end
80
97
 
98
+ def to_s
99
+ "Selector #{super}"
100
+ end
101
+
81
102
  end
82
103
  end
@@ -24,7 +24,7 @@ module Activecube::Query
24
24
  definition.class
25
25
  end
26
26
 
27
- def append_query _cube_query, table, query
27
+ def append_query _model, cube_query, table, query
28
28
 
29
29
  attr_alias = "`#{key.to_s}`"
30
30
  expr = field ? Arel.sql( modifier || field.definition ) : table[dimension_class.column_name]
@@ -33,12 +33,21 @@ module Activecube::Query
33
33
  if identity = dimension_class.identity
34
34
  query = query.project(table[identity]).group(table[identity])
35
35
  else
36
- query = query.group(attr_alias).order(attr_alias)
36
+ query = query.group(attr_alias)
37
+ end
38
+
39
+ if cube_query.orderings.empty?
40
+ query = query.order(attr_alias)
37
41
  end
38
42
 
39
43
  query
40
44
  end
41
45
 
46
+ def to_s
47
+ "Dimension #{super}"
48
+ end
49
+
50
+
42
51
  private
43
52
 
44
53
  def field_methods!
@@ -3,11 +3,38 @@ require 'activecube/query/cube_query'
3
3
  module Activecube
4
4
  module QueryMethods
5
5
 
6
- [:slice, :measure, :when].each do |method|
6
+ attr_reader :database, :role
7
+
8
+ [:slice, :measure, :when, :skip, :take, :desc, :asc].each do |method|
7
9
  define_method(method) do |*args|
8
10
  Query::CubeQuery.new(self).send method, *args
9
11
  end
10
12
  end
11
13
 
14
+ def connected_to database: nil, role: nil, &block
15
+ raise ArgumentError, "Must pass block to method" unless block_given?
16
+ super_model.connected_to(database: database, role: role) do
17
+ @database = database
18
+ @role = role
19
+ block.call self
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+
26
+ def super_model
27
+ raise ArgumentError, "No tables specified for cube #{name}" if tables.count==0
28
+ return tables.first.model if tables.count==1
29
+
30
+ tables.collect{ |t|
31
+ t.model.ancestors.select{|c| c <= ActiveRecord::Base }.reverse
32
+ }.transpose.select{|c|
33
+ c.uniq.count==1
34
+ }.last.first
35
+
36
+ end
37
+
38
+
12
39
  end
13
40
  end
@@ -1,12 +1,12 @@
1
- require 'activecube/dimension_definition_methods'
1
+ require 'activecube/definition_methods'
2
2
 
3
3
  module Activecube
4
4
  class Selector
5
- extend DimensionDefinitionMethods
5
+ extend DefinitionMethods
6
6
 
7
- def expression arel_table, selector, _cube_query
7
+ def expression model, arel_table, selector, _cube_query
8
8
  op = selector.operator
9
- op.expression arel_table[self.class.column_name.to_sym], op.argument
9
+ op.expression model, arel_table[self.class.column_name.to_sym], op.argument
10
10
  end
11
11
 
12
12
  end
@@ -1,3 +1,3 @@
1
1
  module Activecube
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activecube
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksey Studnev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-17 00:00:00.000000000 Z
11
+ date: 2020-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -91,13 +91,13 @@ files:
91
91
  - lib/activecube.rb
92
92
  - lib/activecube/active_record_extension.rb
93
93
  - lib/activecube/base.rb
94
- - lib/activecube/common/count.rb
95
- - lib/activecube/common/sum.rb
94
+ - lib/activecube/common/metrics.rb
96
95
  - lib/activecube/cube_definition.rb
96
+ - lib/activecube/definition_methods.rb
97
97
  - lib/activecube/dimension.rb
98
- - lib/activecube/dimension_definition_methods.rb
99
98
  - lib/activecube/field.rb
100
99
  - lib/activecube/metric.rb
100
+ - lib/activecube/modifier.rb
101
101
  - lib/activecube/processor/composer.rb
102
102
  - lib/activecube/processor/index.rb
103
103
  - lib/activecube/processor/measure_tables.rb
@@ -108,6 +108,7 @@ files:
108
108
  - lib/activecube/query/item.rb
109
109
  - lib/activecube/query/limit.rb
110
110
  - lib/activecube/query/measure.rb
111
+ - lib/activecube/query/modification.rb
111
112
  - lib/activecube/query/or_selector.rb
112
113
  - lib/activecube/query/ordering.rb
113
114
  - lib/activecube/query/selector.rb
@@ -1,10 +0,0 @@
1
- module Activecube::Common
2
-
3
- class Count < Activecube::Metric
4
-
5
- def expression arel_table, measure, cube_query
6
- measure.selectors.empty? ? Arel.star.count : Arel.star.countIf(measure.condition_query arel_table, cube_query)
7
- end
8
-
9
- end
10
- end
@@ -1,11 +0,0 @@
1
- module Activecube::Common
2
-
3
- class Sum < Activecube::Metric
4
-
5
- def expression arel_table, measure, cube_query
6
- column = arel_table[self.class.column_name.to_sym]
7
- measure.selectors.empty? ? column.sum : column.sumIf(measure.condition_query arel_table, cube_query)
8
- end
9
-
10
- end
11
- end