mondrian-olap 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Gemfile +15 -0
- data/LICENSE-Mondrian.html +259 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +219 -0
- data/RUNNING_TESTS.rdoc +41 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/mondrian-olap.rb +1 -0
- data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
- data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
- data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
- data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
- data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
- data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
- data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
- data/lib/mondrian/jars/javacup.jar +0 -0
- data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
- data/lib/mondrian/jars/log4j.properties +18 -0
- data/lib/mondrian/jars/mondrian.jar +0 -0
- data/lib/mondrian/jars/olap4j.jar +0 -0
- data/lib/mondrian/olap.rb +14 -0
- data/lib/mondrian/olap/connection.rb +122 -0
- data/lib/mondrian/olap/cube.rb +236 -0
- data/lib/mondrian/olap/query.rb +313 -0
- data/lib/mondrian/olap/result.rb +155 -0
- data/lib/mondrian/olap/schema.rb +158 -0
- data/lib/mondrian/olap/schema_element.rb +123 -0
- data/mondrian-olap.gemspec +116 -0
- data/spec/connection_spec.rb +56 -0
- data/spec/cube_spec.rb +259 -0
- data/spec/fixtures/MondrianTest.xml +128 -0
- data/spec/fixtures/MondrianTestOracle.xml +128 -0
- data/spec/query_spec.rb +582 -0
- data/spec/rake_tasks.rb +185 -0
- data/spec/schema_definition_spec.rb +345 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/matchers/be_like.rb +24 -0
- 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
|