mondrian-olap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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