mondrian-olap 0.1.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.
Files changed (41) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +15 -0
  3. data/LICENSE-Mondrian.html +259 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.rdoc +219 -0
  6. data/RUNNING_TESTS.rdoc +41 -0
  7. data/Rakefile +46 -0
  8. data/VERSION +1 -0
  9. data/lib/mondrian-olap.rb +1 -0
  10. data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
  11. data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
  12. data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
  13. data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
  14. data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
  15. data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
  16. data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
  17. data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
  18. data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
  19. data/lib/mondrian/jars/javacup.jar +0 -0
  20. data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
  21. data/lib/mondrian/jars/log4j.properties +18 -0
  22. data/lib/mondrian/jars/mondrian.jar +0 -0
  23. data/lib/mondrian/jars/olap4j.jar +0 -0
  24. data/lib/mondrian/olap.rb +14 -0
  25. data/lib/mondrian/olap/connection.rb +122 -0
  26. data/lib/mondrian/olap/cube.rb +236 -0
  27. data/lib/mondrian/olap/query.rb +313 -0
  28. data/lib/mondrian/olap/result.rb +155 -0
  29. data/lib/mondrian/olap/schema.rb +158 -0
  30. data/lib/mondrian/olap/schema_element.rb +123 -0
  31. data/mondrian-olap.gemspec +116 -0
  32. data/spec/connection_spec.rb +56 -0
  33. data/spec/cube_spec.rb +259 -0
  34. data/spec/fixtures/MondrianTest.xml +128 -0
  35. data/spec/fixtures/MondrianTestOracle.xml +128 -0
  36. data/spec/query_spec.rb +582 -0
  37. data/spec/rake_tasks.rb +185 -0
  38. data/spec/schema_definition_spec.rb +345 -0
  39. data/spec/spec_helper.rb +67 -0
  40. data/spec/support/matchers/be_like.rb +24 -0
  41. metadata +217 -0
@@ -0,0 +1,313 @@
1
+ module Mondrian
2
+ module OLAP
3
+ class Query
4
+ def self.from(connection, cube_name)
5
+ query = self.new(connection)
6
+ query.cube_name = cube_name
7
+ query
8
+ end
9
+
10
+ attr_accessor :cube_name
11
+
12
+ def initialize(connection)
13
+ @connection = connection
14
+ @cube = nil
15
+ @axes = []
16
+ @where = []
17
+ @with = []
18
+ end
19
+
20
+ # Add new axis(i) to query
21
+ # or return array of axis(i) members if no arguments specified
22
+ def axis(i, *axis_members)
23
+ if axis_members.empty?
24
+ @axes[i]
25
+ else
26
+ @axes[i] ||= []
27
+ @current_set = @axes[i]
28
+ if axis_members.length == 1 && axis_members[0].is_a?(Array)
29
+ @current_set.concat(axis_members[0])
30
+ else
31
+ @current_set.concat(axis_members)
32
+ end
33
+ self
34
+ end
35
+ end
36
+
37
+ AXIS_ALIASES = %w(columns rows pages sections chapters)
38
+ AXIS_ALIASES.each_with_index do |axis, i|
39
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
+ def #{axis}(*axis_members)
41
+ axis(#{i}, *axis_members)
42
+ end
43
+ RUBY
44
+ end
45
+
46
+ def crossjoin(*axis_members)
47
+ raise ArgumentError, "cannot use crossjoin method before axis or with_set method" unless @current_set
48
+ raise ArgumentError, "specify list of members for crossjoin method" if axis_members.empty?
49
+ members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
50
+ @current_set.replace [:crossjoin, @current_set.clone, members]
51
+ self
52
+ end
53
+
54
+ def except(*axis_members)
55
+ raise ArgumentError, "cannot use except method before axis or with_set method" unless @current_set
56
+ raise ArgumentError, "specify list of members for except method" if axis_members.empty?
57
+ members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
58
+ if @current_set[0] == :crossjoin
59
+ @current_set[2] = [:except, @current_set[2], members]
60
+ else
61
+ @current_set.replace [:except, @current_set.clone, members]
62
+ end
63
+ self
64
+ end
65
+
66
+ def nonempty
67
+ raise ArgumentError, "cannot use nonempty method before axis method" unless @current_set
68
+ @current_set.replace [:nonempty, @current_set.clone]
69
+ self
70
+ end
71
+
72
+ def filter(condition, options={})
73
+ raise ArgumentError, "cannot use filter method before axis or with_set method" unless @current_set
74
+ @current_set.replace [:filter, @current_set.clone, condition]
75
+ @current_set << options[:as] if options[:as]
76
+ self
77
+ end
78
+
79
+ def filter_nonempty
80
+ raise ArgumentError, "cannot use filter_nonempty method before axis or with_set method" unless @current_set
81
+ condition = "NOT ISEMPTY(S.CURRENT)"
82
+ @current_set.replace [:filter, @current_set.clone, condition, 'S']
83
+ self
84
+ end
85
+
86
+ VALID_ORDERS = ['ASC', 'BASC', 'DESC', 'BDESC']
87
+
88
+ def order(expression, direction)
89
+ raise ArgumentError, "cannot use order method before axis or with_set method" unless @current_set
90
+ direction = direction.to_s.upcase
91
+ raise ArgumentError, "invalid order direction #{direction.inspect}," <<
92
+ " should be one of #{VALID_ORDERS.inspect[1..-2]}" unless VALID_ORDERS.include?(direction)
93
+ @current_set.replace [:order, @current_set.clone, expression, direction]
94
+ self
95
+ end
96
+
97
+ %w(top bottom).each do |extreme|
98
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
99
+ def #{extreme}_count(count, expression=nil)
100
+ raise ArgumentError, "cannot use #{extreme}_count method before axis or with_set method" unless @current_set
101
+ @current_set.replace [:#{extreme}_count, @current_set.clone, count]
102
+ @current_set << expression if expression
103
+ self
104
+ end
105
+ RUBY
106
+
107
+ %w(percent sum).each do |extreme_name|
108
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
109
+ def #{extreme}_#{extreme_name}(value, expression)
110
+ raise ArgumentError, "cannot use #{extreme}_#{extreme_name} method before axis or with_set method" unless @current_set
111
+ @current_set.replace [:#{extreme}_#{extreme_name}, @current_set.clone, value, expression]
112
+ self
113
+ end
114
+ RUBY
115
+ end
116
+ end
117
+
118
+ def hierarchize(order=nil, all=nil)
119
+ raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
120
+ order = order && order.to_s.upcase
121
+ raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
122
+ if all.nil? && @current_set[0] == :crossjoin
123
+ @current_set[2] = [:hierarchize, @current_set[2]]
124
+ @current_set[2] << order if order
125
+ else
126
+ @current_set.replace [:hierarchize, @current_set.clone]
127
+ @current_set << order if order
128
+ end
129
+ self
130
+ end
131
+
132
+ def hierarchize_all(order=nil)
133
+ hierarchize(order, :all)
134
+ end
135
+
136
+ # Add new WHERE condition to query
137
+ # or return array of existing conditions if no arguments specified
138
+ def where(*members)
139
+ if members.empty?
140
+ @where
141
+ else
142
+ if members.length == 1 && members[0].is_a?(Array)
143
+ @where.concat(members[0])
144
+ else
145
+ @where.concat(members)
146
+ end
147
+ self
148
+ end
149
+ end
150
+
151
+ # Add definition of calculated member
152
+ def with_member(member_name)
153
+ @with << [:member, member_name]
154
+ @current_set = nil
155
+ self
156
+ end
157
+
158
+ # Add definition of named_set
159
+ def with_set(set_name)
160
+ @current_set = []
161
+ @with << [:set, set_name, @current_set]
162
+ self
163
+ end
164
+
165
+ # return array of member and set definitions
166
+ def with
167
+ @with
168
+ end
169
+
170
+ # Add definition to calculated member or to named set
171
+ def as(*params)
172
+ # definition of named set
173
+ if @current_set
174
+ if params.empty?
175
+ raise ArgumentError, "named set cannot be empty"
176
+ else
177
+ raise ArgumentError, "cannot use 'as' method before with_set method" unless @current_set.empty?
178
+ if params.length == 1 && params[0].is_a?(Array)
179
+ @current_set.concat(params[0])
180
+ else
181
+ @current_set.concat(params)
182
+ end
183
+ end
184
+ # definition of calculated member
185
+ else
186
+ member_definition = @with.last
187
+ options = params.last.is_a?(Hash) ? params.pop : nil
188
+ raise ArgumentError, "cannot use 'as' method before with_member method" unless member_definition &&
189
+ member_definition[0] == :member && member_definition.length == 2
190
+ raise ArgumentError, "calculated member definition should be single expression" unless params.length == 1
191
+ member_definition << params[0]
192
+ member_definition << options if options
193
+ end
194
+ self
195
+ end
196
+
197
+ def to_mdx
198
+ mdx = ""
199
+ mdx << "WITH #{with_to_mdx}\n" unless @with.empty?
200
+ mdx << "SELECT #{axis_to_mdx}\n"
201
+ mdx << "FROM #{from_to_mdx}"
202
+ mdx << "\nWHERE #{where_to_mdx}" unless @where.empty?
203
+ mdx
204
+ end
205
+
206
+ def execute
207
+ @connection.execute to_mdx
208
+ end
209
+
210
+ private
211
+
212
+ # FIXME: keep original order of WITH MEMBER and WITH SET defitions
213
+ def with_to_mdx
214
+ @with.map do |definition|
215
+ case definition[0]
216
+ when :member
217
+ member_name = definition[1]
218
+ expression = definition[2]
219
+ options = definition[3]
220
+ options_string = ''
221
+ options && options.each do |option, value|
222
+ options_string << ", #{option.to_s.upcase} = #{quote_value(value)}"
223
+ end
224
+ "MEMBER #{member_name} AS #{quote_value(expression)}#{options_string}"
225
+ when :set
226
+ set_name = definition[1]
227
+ set_members = definition[2]
228
+ "SET #{set_name} AS #{quote_value(members_to_mdx(set_members))}"
229
+ end
230
+ end.join("\n")
231
+ end
232
+
233
+ def axis_to_mdx
234
+ mdx = ""
235
+ @axes.each_with_index do |axis_members, i|
236
+ axis_name = AXIS_ALIASES[i] ? AXIS_ALIASES[i].upcase : "AXIS(#{i})"
237
+ mdx << ",\n" if i > 0
238
+ mdx << members_to_mdx(axis_members) << " ON " << axis_name
239
+ end
240
+ mdx
241
+ end
242
+
243
+ MDX_FUNCTIONS = {
244
+ :top_count => 'TOPCOUNT',
245
+ :top_percent => 'TOPPERCENT',
246
+ :top_sum => 'TOPSUM',
247
+ :bottom_count => 'BOTTOMCOUNT',
248
+ :bottom_percent => 'BOTTOMPERCENT',
249
+ :bottom_sum => 'BOTTOMSUM'
250
+ }
251
+
252
+ def members_to_mdx(members)
253
+ if members.length == 1
254
+ members[0]
255
+ elsif members[0].is_a?(Symbol)
256
+ case members[0]
257
+ when :crossjoin
258
+ "CROSSJOIN(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
259
+ when :except
260
+ "EXCEPT(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
261
+ when :nonempty
262
+ "NON EMPTY #{members_to_mdx(members[1])}"
263
+ when :filter
264
+ as_alias = members[3] ? " AS #{members[3]}" : nil
265
+ "FILTER(#{members_to_mdx(members[1])}#{as_alias}, #{members[2]})"
266
+ when :order
267
+ "ORDER(#{members_to_mdx(members[1])}, #{expression_to_mdx(members[2])}, #{members[3]})"
268
+ when :top_count, :bottom_count
269
+ mdx = "#{MDX_FUNCTIONS[members[0]]}(#{members_to_mdx(members[1])}, #{members[2]}"
270
+ mdx << (members[3] ? ", #{expression_to_mdx(members[3])})" : ")")
271
+ when :top_percent, :top_sum, :bottom_percent, :bottom_sum
272
+ "#{MDX_FUNCTIONS[members[0]]}(#{members_to_mdx(members[1])}, #{members[2]}, #{expression_to_mdx(members[3])})"
273
+ when :hierarchize
274
+ "HIERARCHIZE(#{members_to_mdx(members[1])}#{members[2] && ", #{members[2]}"})"
275
+ else
276
+ raise ArgumentError, "Cannot generate MDX for invalid set operation #{members[0].inspect}"
277
+ end
278
+ else
279
+ "{#{members.join(', ')}}"
280
+ end
281
+ end
282
+
283
+ def expression_to_mdx(expression)
284
+ expression.is_a?(Array) ? "(#{expression.join(', ')})" : expression
285
+ end
286
+
287
+ def from_to_mdx
288
+ "[#{@cube_name}]"
289
+ end
290
+
291
+ def where_to_mdx
292
+ mdx = '('
293
+ mdx << @where.map do |condition|
294
+ condition
295
+ end.join(', ')
296
+ mdx << ')'
297
+ end
298
+
299
+ def quote_value(value)
300
+ case value
301
+ when String
302
+ "'#{value.gsub("'", "''")}'"
303
+ when TrueClass, FalseClass
304
+ value ? 'TRUE' : 'FALSE'
305
+ when NilClass
306
+ 'NULL'
307
+ else
308
+ "#{value}"
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,155 @@
1
+ require 'nokogiri'
2
+
3
+ module Mondrian
4
+ module OLAP
5
+ class Result
6
+ def initialize(connection, raw_cell_set)
7
+ @connection = connection
8
+ @raw_cell_set = raw_cell_set
9
+ end
10
+
11
+ def axes_count
12
+ axes.length
13
+ end
14
+
15
+ def axis_names
16
+ @axis_names ||= axis_positions(:getName)
17
+ end
18
+
19
+ def axis_full_names
20
+ @axis_full_names ||= axis_positions(:getUniqueName)
21
+ end
22
+
23
+ def axis_members
24
+ @axis_members ||= axis_positions(:to_member)
25
+ end
26
+
27
+ %w(column row page section chapter).each_with_index do |axis, i|
28
+ define_method :"#{axis}_names" do
29
+ axis_names[i]
30
+ end
31
+
32
+ define_method :"#{axis}_full_names" do
33
+ axis_full_names[i]
34
+ end
35
+
36
+ define_method :"#{axis}_members" do
37
+ axis_members[i]
38
+ end
39
+ end
40
+
41
+ def values(*axes_sequence)
42
+ values_using(:getValue, axes_sequence)
43
+ end
44
+
45
+ def formatted_values(*axes_sequence)
46
+ values_using(:getFormattedValue, axes_sequence)
47
+ end
48
+
49
+ def values_using(values_method, axes_sequence = [])
50
+ if axes_sequence.empty?
51
+ axes_sequence = (0...axes_count).to_a.reverse
52
+ elsif axes_sequence.size != axes_count
53
+ raise ArgumentError, "axes sequence size is not equal to result axes count"
54
+ end
55
+ recursive_values(values_method, axes_sequence, 0)
56
+ end
57
+
58
+ # format results in simple HTML table
59
+ def to_html
60
+ case axes_count
61
+ when 1
62
+ builder = Nokogiri::XML::Builder.new do |doc|
63
+ doc.table do
64
+ doc.tr do
65
+ column_full_names.each do |column_full_name|
66
+ column_full_name = column_full_name.join(',') if column_full_name.is_a?(Array)
67
+ doc.th column_full_name, :align => 'right'
68
+ end
69
+ end
70
+ doc.tr do
71
+ values.each do |value|
72
+ doc.td value, :align => 'right'
73
+ end
74
+ end
75
+ end
76
+ end
77
+ builder.doc.to_html
78
+ when 2
79
+ builder = Nokogiri::XML::Builder.new do |doc|
80
+ doc.table do
81
+ doc.tr do
82
+ doc.th
83
+ column_full_names.each do |column_full_name|
84
+ column_full_name = column_full_name.join(',') if column_full_name.is_a?(Array)
85
+ doc.th column_full_name, :align => 'right'
86
+ end
87
+ end
88
+ values.each_with_index do |row, i|
89
+ doc.tr do
90
+ row_full_name = row_full_names[i].is_a?(Array) ? row_full_names[i].join(',') : row_full_names[i]
91
+ doc.th row_full_name, :align => 'left'
92
+ row.each do |cell|
93
+ doc.td cell, :align => 'right'
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ builder.doc.to_html
100
+ else
101
+ raise ArgumentError, "just columns and rows axes are supported"
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def axes
108
+ @axes ||= @raw_cell_set.getAxes
109
+ end
110
+
111
+ def axis_positions(map_method, join_with=false)
112
+ axes.map do |axis|
113
+ axis.getPositions.map do |position|
114
+ names = position.getMembers.map do |member|
115
+ if map_method == :to_member
116
+ Member.new(member)
117
+ else
118
+ member.send(map_method)
119
+ end
120
+ end
121
+ if names.size == 1
122
+ names[0]
123
+ elsif join_with
124
+ names.join(join_with)
125
+ else
126
+ names
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ AXIS_SYMBOL_TO_NUMBER = {
133
+ :columns => 0,
134
+ :rows => 1,
135
+ :pages => 2,
136
+ :sections => 3,
137
+ :chapters => 4
138
+ }.freeze
139
+
140
+ def recursive_values(value_method, axes_sequence, current_index, cell_params=[])
141
+ if axis_number = axes_sequence[current_index]
142
+ axis_number = AXIS_SYMBOL_TO_NUMBER[axis_number] if axis_number.is_a?(Symbol)
143
+ positions_size = axes[axis_number].getPositions.size
144
+ (0...positions_size).map do |i|
145
+ cell_params[axis_number] = Java::JavaLang::Integer.new(i)
146
+ recursive_values(value_method, axes_sequence, current_index + 1, cell_params)
147
+ end
148
+ else
149
+ @raw_cell_set.getCell(cell_params).send(value_method)
150
+ end
151
+ end
152
+
153
+ end
154
+ end
155
+ end