martyr 0.1.74.pre

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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.tags +868 -0
  7. data/.travis.yml +3 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +265 -0
  11. data/Rakefile +1 -0
  12. data/TODO.txt +54 -0
  13. data/bin/console +62 -0
  14. data/bin/setup +7 -0
  15. data/lib/martyr/base_cube.rb +73 -0
  16. data/lib/martyr/cube.rb +134 -0
  17. data/lib/martyr/dimension_reference.rb +26 -0
  18. data/lib/martyr/errors.rb +20 -0
  19. data/lib/martyr/helpers/delegators.rb +17 -0
  20. data/lib/martyr/helpers/intervals.rb +222 -0
  21. data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
  22. data/lib/martyr/helpers/registrable.rb +15 -0
  23. data/lib/martyr/helpers/sorter.rb +79 -0
  24. data/lib/martyr/helpers/translations.rb +34 -0
  25. data/lib/martyr/level_concern/has_level_collection.rb +11 -0
  26. data/lib/martyr/level_concern/level.rb +45 -0
  27. data/lib/martyr/level_concern/level_collection.rb +60 -0
  28. data/lib/martyr/level_concern/level_comparator.rb +45 -0
  29. data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
  30. data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
  31. data/lib/martyr/runtime/data_set/element.rb +66 -0
  32. data/lib/martyr/runtime/data_set/element_common.rb +51 -0
  33. data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
  34. data/lib/martyr/runtime/data_set/fact.rb +83 -0
  35. data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
  36. data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
  37. data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
  38. data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
  39. data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
  40. data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
  41. data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
  42. data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
  43. data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
  44. data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
  45. data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
  46. data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
  47. data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
  48. data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
  49. data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
  50. data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
  51. data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
  52. data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
  53. data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
  54. data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
  55. data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
  56. data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
  57. data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
  58. data/lib/martyr/runtime/query/query_context.rb +246 -0
  59. data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
  60. data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
  61. data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
  62. data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
  63. data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
  64. data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
  65. data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
  66. data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
  67. data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
  68. data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
  69. data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
  70. data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
  71. data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
  72. data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
  73. data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
  74. data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
  75. data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
  76. data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
  77. data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
  78. data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
  79. data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
  80. data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
  81. data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
  82. data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
  83. data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
  84. data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
  85. data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
  86. data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
  87. data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
  88. data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
  89. data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
  90. data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
  91. data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
  92. data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
  93. data/lib/martyr/schema/metrics/base_metric.rb +77 -0
  94. data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
  95. data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
  96. data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
  97. data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
  98. data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
  99. data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
  100. data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
  101. data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
  102. data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
  103. data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
  104. data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
  105. data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
  106. data/lib/martyr/version.rb +3 -0
  107. data/lib/martyr/virtual_cube.rb +74 -0
  108. data/lib/martyr.rb +55 -0
  109. data/martyr.gemspec +41 -0
  110. metadata +296 -0
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in martyr.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Amit Aharoni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # Martyr
2
+ A multi-dimensional semantic layer on top of ActiveRecord that allows running pivot table queries and rendering
3
+ them as CSV, HTML, or KickChart-ready hashes. Supports time dimensions, cohort analysis, custom rollups, and drilling
4
+ through to the underlying ActiveRecord objects.
5
+
6
+ Designed predominantly to allow staff members and site owners to slice and dice data and easily produce reports.
7
+
8
+ Warning:
9
+ This gem is not production ready.
10
+
11
+ More to come...
12
+
13
+ ## The problem
14
+ The following simple queries demonstrate a core issue with relational data:
15
+
16
+ ```ruby
17
+ # Fetch count of all posts that were created 3 days ago
18
+ Post.where('created_at > ?', 3.days.ago)
19
+
20
+ # Fetch count of all posts that were created by the author whose email is me@domain.com
21
+ Post.joins(:author).where('authors.email' => 'me@domain.com')
22
+ ```
23
+ These two queries represent similar requests from the perspective of a human user. However, fulfilling these requests
24
+ requires knowledge about the structure of the database. This limits the ability to provide "free form" slicing and dicing of data.
25
+
26
+ Compare these queries with Martyr standard query interface:
27
+ ```ruby
28
+ # Fetch count of all posts that were created 3 days ago
29
+ PostCube.slice('post.created_at', gt: 3.days.ago)
30
+
31
+ # Fetch count of all posts that were created by the author whose email is me@domain.com
32
+ PostCube.slice('author.email', with: 'me@domain.com')
33
+
34
+ # As a compound hash format:
35
+ PostCube.slice('author.email' => {with: 'me@domain.com'}, 'post.created_at' => {gt: 3.days.ago})
36
+ ```
37
+ The last example demonstrates an important aspect of Martyr. A simple standard hash - which can be easily
38
+ serialized over HTTP requests - represents a slice of data that can be further manipulated. This means you can have
39
+ one standard controller action for displaying tables and graphs of any data you like.
40
+
41
+ Martyr keeps track of the slice of every element you query:
42
+
43
+ ```ruby
44
+ pivot_table = PostCube.slice('post.created_at', gt: 3.days.ago).pivot.on_columns('post_count').on_rows('authors.email')
45
+ first_row = pivot_table.rows.first
46
+ cell = first_row.cell_at['post_count']
47
+ cell.coordinates
48
+ # => { 'metric' => 'posts_cube.post_count', 'post.created_at' => {gt: 3.days.ago}, 'authors.email' => {with: 'first_row@domain.com'} }
49
+ ```
50
+
51
+ So an ERB partial that renders this cell as a link to another report - number of comments per post-type for that particular author and created_at constraints - may look like this:
52
+ ```ERB
53
+ <% data = Base64.urlsafe_encode64 {current_slice: cell.coordinates,
54
+ on_rows: ['post.type'],
55
+ on_columns: ['comment.type'],
56
+ view: 'my_drill_down_report' }.to_json %>
57
+
58
+ <%= link_to cell.value, standard_report_action_path(data: data) %>
59
+ ```
60
+
61
+ And the standard (non-secure) controller action can be as simple as:
62
+ ```ruby
63
+ def standard_drill_down
64
+ hash = JSON.parse(Base64.urlsafe_decode64(params[:data]}
65
+ query = PostCube.slice(hash['current_slice']).granulate(hash['on_rows'] + hash['on_columns'])
66
+ @pivot_table = query.pivot.on_columns(hash['on_columns']).on_rows(hash['on_rows'])
67
+ render hash['view']
68
+ end
69
+ ```
70
+
71
+
72
+ The examples that follow refer to the open source Chinook database schema.
73
+
74
+ ## Semantic layer DSL
75
+
76
+ ```ruby
77
+ class SharedDimensions < Martyr::Cube
78
+ define_dimension :genres do
79
+ degenerate_level :name
80
+ end
81
+
82
+ define_dimension :media_types do
83
+ degenerate_level :name
84
+ end
85
+
86
+ define_dimension :customers do
87
+ degenerate_level :country
88
+ degenerate_level :state
89
+ degenerate_level :city
90
+ query_level :last_name, -> { Customer.all }, fact_key: 'customers.id'
91
+ end
92
+ end
93
+
94
+ class MyCube < SharedDimensions
95
+ has_dimension_level :genres, :name
96
+ has_dimension_level :media_types, :name
97
+ has_dimension_level :customers, :last_name
98
+
99
+ has_sum_metric :units_sold, 'SUM(invoice_lines.quantity)'
100
+ has_sum_metric :amount, 'SUM(invoice_lines.unit_price * invoice_lines.quantity)'
101
+
102
+ has_custom_metric :commission, ->(fact) { (fact['amount'] * 0.3) }
103
+
104
+ has_custom_rollup :avg_transaction, ->(fact_set) { fact_set['amount'] / fact_set['units_sold'] }
105
+ has_custom_rollup :usa_amount, ->(fact_set) { fact_set.locate('customers.country', with: 'USA')['amount'] }
106
+ has_custom_rollup :cross_country_avg_transaction, ->(fact_set) { fact_set.locate(reset: 'customers.*')['avg_transaction'] }
107
+ has_custom_rollup :usa_avg_transaction, ->(fact_set) { fact_set.locate('customers.country', with: 'USA')['avg_transaction'] }
108
+
109
+ main_query do
110
+ InvoiceLine.joins(track: [:genre, :media_type], invoice: :customer)
111
+ end
112
+ end
113
+ ```
114
+
115
+ ## Query interface
116
+
117
+ Use `#granulate` to perform group by:
118
+ ```ruby
119
+ q = MyCube.granulate('genres.name', 'customers.city').pivot.on_columns(:metrics).on_rows('genres.name', 'customers.city')
120
+ q.to_csv
121
+ q.to_chart # => Kick chart format
122
+ ```
123
+
124
+ Use `#select` to choose metrics and `#slice` to perform where:
125
+ ```ruby
126
+ MyCube.select('units_sold', 'amount').
127
+ granulate('genres.name', 'customers.city').
128
+ slice('customers.country', with: 'USA').
129
+ pivot.on_columns(:metrics).on_rows('genres.name', 'customers.city').to_csv
130
+ ```
131
+
132
+ The same thing can be done in stages:
133
+ ```ruby
134
+ query = MyCube.select('units_sold', 'amount').
135
+ granulate('genres.name', 'customers.city').
136
+ slice('customers.country', with: 'USA').build
137
+
138
+ query.pivot.on_columns(:metrics).on_rows('genres.name', 'customers.city').to_csv
139
+ ```
140
+
141
+ Which allows further slicing in memory - in the same standard way - rather than in SQL:
142
+ ```ruby
143
+ query = MyCube.select('units_sold', 'amount').granulate('genres.name', 'customers.city').build
144
+ usa_slice = query.slice('customers.country', with: 'USA')
145
+ france_slice = query.slice('customers.country', with: 'France')
146
+
147
+ # One DB query, two reports
148
+ usa_slice.pivot.on_columns('genres.name').on_rows('customers.city').in_cells('units_sold').to_csv
149
+ france_slice.pivot.on_columns('genres.name').on_rows(:metrics).to_csv
150
+ ```
151
+
152
+ ## Virtual cubes DSL
153
+
154
+ Virtual cubes allow referring to two or more cubes, each with different granularity, on the same report.
155
+ ```ruby
156
+ class MyOtherCube < SharedDimensions
157
+ define_dimension :playlists do
158
+ query_level :name, -> { Playlist.all }, label_key: 'name'
159
+ end
160
+
161
+ has_dimension_level :genres, :name
162
+ has_dimension_level :media_types, :name
163
+ has_dimension_level :playlists, :name, fact_key: 'playlists.id'
164
+
165
+ has_sum_metric :tracks_count, 'COUNT(playlists.id)'
166
+
167
+ main_query do
168
+ Playlist.joins(tracks: [:genre, :media_type])
169
+ end
170
+ end
171
+
172
+ class MyVirtualCube < Martyr::VirtualCube
173
+ use_cube 'MyCube'
174
+ use_cube 'MyOtherCube'
175
+ end
176
+ ```
177
+
178
+ ## Using virtual cubes
179
+
180
+ Usage is the same as regular cubes.
181
+ ```ruby
182
+ query = MyVirtualCube.granulate('genres.name').on_columns('genres.name').on_rows('my_other_cube.tracks_count', 'my_cube.units_sold')
183
+ ```
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+ ## Installation
192
+
193
+ Add this line to your application's Gemfile:
194
+
195
+ ```ruby
196
+ gem 'martyr'
197
+ ```
198
+
199
+ And then execute:
200
+
201
+ $ bundle
202
+
203
+ Or install it yourself as:
204
+
205
+ $ gem install martyr
206
+
207
+ ## Usage
208
+
209
+ TODO: Write usage instructions here
210
+
211
+ ## Development
212
+
213
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
214
+
215
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
216
+
217
+ ## Contributing
218
+
219
+ 1. Fork it ( https://github.com/[my-github-username]/martyr/fork )
220
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
221
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
222
+ 4. Push to the branch (`git push origin my-new-feature`)
223
+ 5. Create a new Pull Request
224
+
225
+
226
+ Limitations:
227
+ - If you want to connect degenerate levels directly to the fact, make sure that they are unique across the entire
228
+ dimension.
229
+
230
+ For example, consider a simple dimension 'customers.country', 'customers.city', 'customers.name', and that
231
+ 'customers.name' is a query level, and the other levels are degenerates.
232
+
233
+ Problem:
234
+ If the cube connects with 'customers.city', the grain is 'customers.city', and the slice is 'customers.city' => 'Dover',
235
+ When you run `facts.first['customers.country']` you may receive either US or UK (if Dover appears once for each in
236
+ the dimension).
237
+
238
+ The reason is that Martyr needs a query level in order to resolve higher degenerate levels that do not connect to
239
+ the fact. To do that, it maintains one "representative" 'customers.name' record for each degenerate 'customers.city'
240
+ value. It then asks this representative to fetch its 'customers.country', which could be either US or UK, depending
241
+ on the randomly chosen representative.
242
+
243
+ Workaround 1:
244
+ Connect 'customers.country' to the fact.
245
+ Martyr always prefer to take degenerates from the facts if they are available.
246
+
247
+ Workaround 2:
248
+ Connect 'customers.name' to the cube and add it to the grain.
249
+ Martyr will prefer to use the query level 'customers.name' when resolving for 'customers.country'.
250
+
251
+ Reproduce:
252
+ c = Customer.create first_name: 'Amit', last_name: 'Aharoni', city: 'Paris', country: 'Hungary', email: 'amit.sites@gmail.com'
253
+ i = Invoice.create customer_id: c.id, invoice_date: Time.now, billing_country: 'Hungary', billing_city: 'Paris', total: 100
254
+ InvoiceLine.create invoice_id: i.id, track_id: 1, unit_price: 1, quantity: 100
255
+
256
+ sub_cube1 = MartyrSpec::DegeneratesAndNoQueryLevel.granulate('customers.city', 'genres.name').slice('customers.city', with: 'Paris').build.sub_cubes.first
257
+ sub_cube2 = MartyrSpec::DegeneratesAndAllLevels.granulate('customers.city', 'genres.name').slice('customers.city', with: 'Paris').build.sub_cubes.first
258
+ sub_cube3 = MartyrSpec::NoHighLevels.granulate('customers.last_name', 'genres.name').slice('customers.city', with: 'Paris').build.sub_cubes.first
259
+
260
+ sub_cube1.elements(levels: ['customers.city', 'customers.country']).count
261
+ # => 1
262
+ sub_cube2.elements(levels: ['customers.city', 'customers.country']).count
263
+ # => 2
264
+ sub_cube3.elements(levels: ['customers.city', 'customers.country']).count
265
+ # => 2
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/TODO.txt ADDED
@@ -0,0 +1,54 @@
1
+
2
+ Add ability to add includes directive to an existing dimension scope.
3
+
4
+ Add ability to sort results
5
+
6
+ ============================
7
+
8
+ - QueryContext currently only supports one #slice operation. It should really return a decorator of itself that allies
9
+ the memory slice on it.
10
+
11
+ - The #for_cube_name method on both memory slices and data slices is dangerous - once the block returns the object's
12
+ scoping is useless. Create either one decorator for both, or a different decorator for each one that acts as a filter.
13
+
14
+ - Coordinate currently communicate with the memory slice in order to reset and set its values. This is useless, memory
15
+ slice should be immutable to the cells, otherwise custom rollups that reset dimension level on a memory slice 2:
16
+ <data slice 1> + <memory slice 2> will return a different value than the same custom rollup on data slice 3:
17
+ <data slice 3> = <data slice 1> + <memory slice 2> though they should be the same.
18
+
19
+ E.g., the rollup:
20
+ has_custom_rollup :usa_amount, ->(fact_set) { fact_set.locate('customers.country', with: 'USA')['amount'] }
21
+
22
+ Will have different result for these two:
23
+ Cube.slice('customers.country', with: 'France').build # => rollup will not be able to locate USA element
24
+ Cube.build.slice('customers.country', with: 'France') # => rollup will be able to locate USA element
25
+
26
+ - Virtual cube builder needs fixing. The #build method with all combinations is not working right. It should be
27
+ transitioned to something more performing that does not rely on all possible combinations.
28
+
29
+ - Coordinate, ElementLocator and FactIndexer all do things very similar. Why do we need ElementLocator when we have
30
+ FactIndexer? Does Coordinates really need to know about MemorySlice? Can ElementLocator have an #all method?
31
+
32
+
33
+ If memory slice is immutable, while FactIndexer holds all facts and is able to address multiple memory slices,
34
+ it makes sense to have an element locator that is cube-specific and memory-slice-specific, so the user does not need
35
+ to provide the memory slice every time a cell is needed.
36
+ In this case, the locator is injected to the element to perform location search operations.
37
+
38
+ Coordinates does not need to contain a memory slice. Any attempt to go outside the slice will yield an empty cell.
39
+ So it's simply a hash. We can further remove the knowledge of coordinates about what is a level and what is a
40
+ metric by having this be resolved by the ElementLocator (it has to know fact indexer anyways).
41
+
42
+ VirtualElement #locate will continue to be as today - delegating to the individual locators.
43
+
44
+ VirtualElementBuilder can be simplified - it can simply be provided with the locators, which now will know how to
45
+ fetch all elements if necessary. --> Although: locator should not hold the level ids.. elements grain is really
46
+ based on the pivot grain which can be different than the sub cube grain, and can change dynamically based on whether
47
+ totals are calculated. So perhaps it's better to have the locator be unrelated to the #elements operation.
48
+ Actually, it can be injected into elements instead of memory slice!
49
+
50
+ Standardization of metric names can be moved to the locator too, so "dirty" user input is handled in one place.
51
+
52
+ - Due to the way the grain is initialized in cubes, I'm not sure if currently a virtual cube containing 2 cubes one
53
+ of which contains one more detailed level of granularity is going to work (select_supported_level_ids selects
54
+ all levels in supported dimensions, regardless of whether the level itself can actually be fetched)
data/bin/console ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "martyr"
5
+ require "chinook_database"
6
+ ChinookDatabase.connect
7
+ require_relative "../spec/models/spec_models"
8
+
9
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
10
+
11
+ module Martyr
12
+ def self.reload!
13
+ Reloader.new(self).reload
14
+ end
15
+
16
+ class Reloader
17
+ def initialize(top)
18
+ @top = top
19
+ end
20
+
21
+ def reload
22
+ cleanup
23
+ load_all
24
+ end
25
+
26
+ private
27
+
28
+ def all_project_objects_lookup
29
+ @_all_project_objects_lookup ||= Hash[all_project_objects.map{|x| [x, true]}]
30
+ end
31
+
32
+ def all_project_objects(current = @top)
33
+ return [] unless current.is_a?(Module) and current.to_s.split('::').first == @top.to_s
34
+ [current] + current.constants.flat_map{|x| all_project_objects(current.const_get(x))}
35
+ end
36
+
37
+ def cleanup(parent = Object, current = @top)
38
+ return unless all_project_objects_lookup[current]
39
+ current.constants.each {|const| cleanup current, current.const_get(const)}
40
+ parent.send(:remove_const, current.to_s.split('::').last.to_sym)
41
+ end
42
+
43
+ def loaded_files
44
+ $LOADED_FEATURES.select{|x| x.starts_with?(File.expand_path('../../lib/martyr'))}
45
+ end
46
+
47
+ def load_all
48
+ loaded_files.each{|x| load x}
49
+ true
50
+ end
51
+ end
52
+ end
53
+
54
+ def reload!
55
+ Martyr.reload!
56
+ end
57
+
58
+ require "pry"
59
+ Pry.start
60
+
61
+ # require "irb"
62
+ # IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,73 @@
1
+ module Martyr
2
+ class BaseCube
3
+ extend Martyr::Translations
4
+
5
+ def self.set_cube_name(value)
6
+ @name = value.to_s
7
+ end
8
+
9
+ def self.cube_name
10
+ @name || name.split('::').last.underscore
11
+ end
12
+
13
+ def self.contained_cube_classes
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def self.new_query_context_builder
18
+ Runtime::QueryContextBuilder.new(self, @named_scopes.try(:query_helper_module))
19
+ end
20
+
21
+ # @return [NamedScopedCollection] and extends for the first time the helper module used to delegate scope names to
22
+ # a new query context builder
23
+ def self.named_scopes
24
+ return @named_scopes if @named_scopes
25
+ @named_scopes = Schema::NamedScopeCollection.new
26
+ extend(@named_scopes.cube_helper_module)
27
+ @named_scopes
28
+ end
29
+
30
+ class << self
31
+ delegate :scope, to: :named_scopes
32
+ end
33
+
34
+ def self.dimension_definitions
35
+ {}
36
+ end
37
+
38
+ def self.supported_dimension_definitions
39
+ raise NotImplementedError
40
+ end
41
+
42
+ # @param dimension_names
43
+ # @return [Runtime::DimensionScopeCollection] a collection with dimensions that maintain SQL scoped queries and
44
+ # their results. All dimensions that have at least one level supported by the cube are included.
45
+ def self.build_dimension_scopes(dimension_names)
46
+ dimension_scopes = Runtime::DimensionScopeCollection.new(dimension_definitions)
47
+ supported_dimension_definitions.slice(*dimension_names).values.flat_map(&:level_objects).each do |level|
48
+ dimension_scopes.register_level(level)
49
+ end
50
+ dimension_scopes
51
+ end
52
+
53
+ def self.find_metric(metric_name)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # @param metric_id [String] fully qualified metric ID
58
+ # @return [MetricDefinition]
59
+ def self.find_metric_id(metric_id)
60
+ cube_name, metric_name = id_components(metric_id)
61
+ raise(Schema::Error.new "Metric for cube `#{cube_name}` was asked for cube `#{self.cube_name}`") unless
62
+ cube_name == self.cube_name
63
+
64
+ find_metric(metric_name)
65
+ end
66
+
67
+ # @param metric_id [String] fully qualified metric ID
68
+ # @return [Boolean]
69
+ def self.metric?(metric_id)
70
+ find_metric_id(metric_id).present? rescue false
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,134 @@
1
+ module Martyr
2
+ class Cube < BaseCube
3
+ extend Martyr::LevelComparator
4
+
5
+ def self.contained_cube_classes
6
+ [self]
7
+ end
8
+
9
+ def self.fact_definitions
10
+ @fact_definitions ||= Schema::FactDefinitionCollection.new(self)
11
+ end
12
+
13
+ # Return all dimension definitions regardless of whether they are supported by the cube.
14
+ # A cube +supports+ a dimension only if it calls the #has_dimension_level DSL method.
15
+ # It is quite typical to have multiple cubes inherenting from a context with shared dimensions.
16
+ # So it is typical to have cubes that maintain a reference to a dimension definition they do not support.
17
+ #
18
+ # @see #supported_dimension_definitions to get only the supported dimensions.
19
+ #
20
+ # In the example below, Cube1 does not support shared_dimension_2:
21
+ #
22
+ # class Common < Martyr::Cube
23
+ # define_dimension :shared_dimension_1 do
24
+ # ...
25
+ # end
26
+ #
27
+ # define_dimension :shared_dimension_2 do
28
+ # ...
29
+ # end
30
+ # end
31
+ #
32
+ # class Cube1 < Common
33
+ # has_dimension_level :shared_dimension_1, :level1
34
+ # end
35
+ #
36
+ # Cube1.dimension_definitions.keys
37
+ # # => ['shared_dimension_1', 'shared_dimension_2']
38
+ #
39
+ # @return [Schema::DimensionDefinitionCollection]
40
+ def self.dimension_definitions
41
+ return @dimension_definitions if @dimension_definitions
42
+ @dimension_definitions = Schema::DimensionDefinitionCollection.new
43
+ @dimension_definitions.merge! parent_schema_class.dimension_definitions if parent_schema_class.present?
44
+ @dimension_definitions
45
+ end
46
+
47
+ def self.set_default_fact_grain(*level_ids_arr)
48
+ @default_fact_grain = level_ids_arr
49
+ end
50
+
51
+ def self.default_fact_grain
52
+ @default_fact_grain || []
53
+ end
54
+
55
+ # @return [Array<LevelAssociation>]
56
+ def self.default_fact_grain_level_associations
57
+ level_association_lookup = level_associations.index_by(&:id)
58
+ default_fact_grain.map do |x|
59
+ level_association_lookup[x] || raise(Schema::Error.new("`#{x}` is in the default fact grain but not connected to the fact query"))
60
+ end
61
+ end
62
+
63
+ class << self
64
+ delegate :define_dimension, to: :dimension_definitions
65
+ delegate :main_fact, :build_fact_scopes, :sub_query, to: :fact_definitions
66
+ delegate :has_dimension_level, :has_count_distinct_metric, :has_min_metric, :has_max_metric, # DSL
67
+ :has_sum_metric, :has_custom_metric, :has_custom_rollup, :main_query, # DSL
68
+ :metrics, :find_metric, :dimension_associations, to: :main_fact # Runtime
69
+
70
+ delegate :select, :slice, :granulate, :pivot, :build, to: :new_query_context_builder
71
+ alias_method :all, :new_query_context_builder
72
+ end
73
+
74
+ def self.martyr_schema_class?
75
+ true
76
+ end
77
+
78
+ def self.virtual?
79
+ false
80
+ end
81
+
82
+ # @return [nil, Base]
83
+ def self.parent_schema_class
84
+ ancestors[1..-1].find { |x| x != self and x.respond_to?(:martyr_schema_class?) }
85
+ end
86
+
87
+ # @see comment for #dimension_definitions
88
+ #
89
+ # @return [Schema::DimensionDefinitionCollection] including dimensions that have at least one level
90
+ # supported by the cube through #has_dimension_level
91
+ def self.supported_dimension_definitions
92
+ dimension_definitions.slice(*dimension_associations.keys)
93
+ end
94
+
95
+ # Return all levels that are directly connected to the cube with #has_dimension_level.
96
+ #
97
+ # @return [Array<LevelAssociation>]
98
+ def self.level_associations
99
+ dimension_associations.flat_map { |_name, dimension_association| dimension_association.level_objects }
100
+ end
101
+
102
+ # @return [Array<BaseLevelDefinition>]
103
+ def self.supported_level_definitions
104
+ lowest_level_of(level_associations).flat_map do |level_association|
105
+ level_association.level_definition.level_and_above
106
+ end
107
+ end
108
+
109
+ # @return [Array<String>]
110
+ def self.supported_level_ids
111
+ supported_level_definitions.map(&:id)
112
+ end
113
+
114
+ # Helper methods used to filter out unsupported levels.
115
+ #
116
+ # @param level_ids [Array<String>]
117
+ # @return [Array<String>] all ids that are supported by the cube through the dimension
118
+ def self.select_supported_level_ids(level_ids)
119
+ level_ids = Array.wrap(level_ids)
120
+ unsupported = level_ids - supported_level_ids
121
+ level_ids - unsupported
122
+ end
123
+
124
+ # @return [Schema::DependencyInferrer]
125
+ def self.metric_dependency_inferrer
126
+ @metric_dependency_inferrer ||= Schema::DependencyInferrer.new.add_cube_levels(self)
127
+ end
128
+
129
+ def self.standardizer
130
+ @standardizer ||= Martyr::MetricIdStandardizer.new(cube_name, raise_if_not_ok: false)
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,26 @@
1
+ module Martyr
2
+ class DimensionReference
3
+ attr_reader :dimension_definition
4
+ delegate :name, to: :dimension_definition
5
+
6
+ include Martyr::HasLevelCollection
7
+
8
+ delegate :lowest_level, :level_above, :find_level, :level_names, :level_objects, :has_level?,
9
+ :has_dimension_level, :register_level, to: :levels
10
+
11
+ # For reflection
12
+ def dimension?
13
+ true
14
+ end
15
+
16
+ def initialize(dimension_definition, levels_collection_class, &block)
17
+ @dimension_definition = dimension_definition
18
+ @levels = levels_collection_class.new(dimension: self, &block)
19
+ end
20
+
21
+ # @param mod [Module]
22
+ def register_element_helper_methods(mod)
23
+ level_objects.each {|l| l.register_element_helper_methods(mod)}
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Martyr
2
+ class Error < StandardError
3
+ end
4
+
5
+ module Internal
6
+ class Error < ::Martyr::Error
7
+ end
8
+ end
9
+
10
+ module Schema
11
+ class Error < ::Martyr::Error
12
+ end
13
+ end
14
+
15
+ module Query
16
+ class Error < ::Martyr::Error
17
+ end
18
+ end
19
+
20
+ end