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.
- 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
|