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
data/RUNNING_TESTS.rdoc
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
== Creating test database
|
2
|
+
|
3
|
+
By default unit tests use MySQL database but PostgreSQL and Oracle databases are supported as well. Set MONDRIAN_DRIVER environment variable to "mysql" (default), "postgresql" or "oracle" to specify database driver that should be used.
|
4
|
+
|
5
|
+
If using MySQL or PostgreSQL database then create database user mondrian_test with password mondrian_test, create database mondrian_test and grant full access to this database for mondrian_test user. By default it is assumed that database is located on localhost (can be overridden with DATABASE_HOST environment variable).
|
6
|
+
|
7
|
+
If using Oracle database then create database user mondrian_test with password mondrian_test. By default it is assumed that database orcl is located on localhost (can be overridden with DATABASE_NAME and DATABASE_HOST environment variables).
|
8
|
+
|
9
|
+
See spec/spec_helper.rb for details of default connection parameters and how to override them.
|
10
|
+
|
11
|
+
== Creating test data
|
12
|
+
|
13
|
+
Install necessary gems with
|
14
|
+
|
15
|
+
bundle install
|
16
|
+
|
17
|
+
Create tables with test data using
|
18
|
+
|
19
|
+
rake db:create_data
|
20
|
+
|
21
|
+
or specify which database driver to use
|
22
|
+
|
23
|
+
rake db:create_data MONDRIAN_DRIVER=mysql
|
24
|
+
rake db:create_data MONDRIAN_DRIVER=postgresql
|
25
|
+
rake db:create_data MONDRIAN_DRIVER=oracle
|
26
|
+
|
27
|
+
== Running tests
|
28
|
+
|
29
|
+
Run tests with
|
30
|
+
|
31
|
+
rake spec
|
32
|
+
|
33
|
+
or specify which database driver to use
|
34
|
+
|
35
|
+
rake spec MONDRIAN_DRIVER=mysql
|
36
|
+
rake spec MONDRIAN_DRIVER=postgresql
|
37
|
+
rake spec MONDRIAN_DRIVER=oracle
|
38
|
+
|
39
|
+
== JRuby versions
|
40
|
+
|
41
|
+
It is recommended to use RVM (http://rvm.beginrescueend.com) to run tests with different JRuby implementations. mondrian-olap has been tested with JRuby 1.5 and 1.6 release candidate versions.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = "mondrian-olap"
|
16
|
+
gem.summary = "JRuby API for Mondrian OLAP Java library"
|
17
|
+
gem.description = <<-EOS
|
18
|
+
JRuby gem for performing multidimensional queries of relational database data using Mondrian OLAP Java library
|
19
|
+
EOS
|
20
|
+
gem.email = "raimonds.simanovskis@gmail.com"
|
21
|
+
gem.homepage = "http://github.com/rsim/mondrian-olap"
|
22
|
+
gem.authors = ["Raimonds Simanovskis"]
|
23
|
+
end
|
24
|
+
Jeweler::RubygemsDotOrgTasks.new
|
25
|
+
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
RSpec::Core::RakeTask.new(:spec)
|
28
|
+
|
29
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
30
|
+
t.rcov = true
|
31
|
+
t.rcov_opts = ['--exclude', '/Library,spec/']
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
require 'rake/rdoctask'
|
37
|
+
Rake::RDocTask.new do |rdoc|
|
38
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
39
|
+
|
40
|
+
rdoc.rdoc_dir = 'doc'
|
41
|
+
rdoc.title = "mondrian-olap #{version}"
|
42
|
+
rdoc.rdoc_files.include('README*')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'spec/rake_tasks'
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'mondrian/olap'
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#
|
2
|
+
# Log4J Konfiguration
|
3
|
+
#
|
4
|
+
# - Logs errors on the console
|
5
|
+
#
|
6
|
+
|
7
|
+
log4j.rootLogger = ERROR, A1
|
8
|
+
|
9
|
+
# Logging to console
|
10
|
+
log4j.appender.A1 = org.apache.log4j.ConsoleAppender
|
11
|
+
|
12
|
+
# Logging message format
|
13
|
+
# %d{DATE} Datum im Format dd MMM YYYY HH:mm:ss,SSS
|
14
|
+
# %-5p Priorität der Meldung 5stellig
|
15
|
+
# %m Meldung
|
16
|
+
# %n Zeilenumbruch
|
17
|
+
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
|
18
|
+
# log4j.appender.A1.layout.ConversionPattern=[JPivot] %d{DATE} %-5p [Session %X{SessionID}] %C#%M: %m%n
|
Binary file
|
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
directory = File.expand_path("../jars", __FILE__)
|
4
|
+
Dir["#{directory}/*.jar"].each do |file|
|
5
|
+
require file
|
6
|
+
end
|
7
|
+
|
8
|
+
java.lang.System.setProperty("log4j.configuration", "file://#{directory}/log4j.properties")
|
9
|
+
# register Mondrian olap4j driver
|
10
|
+
Java::mondrian.olap4j.MondrianOlap4jDriver
|
11
|
+
|
12
|
+
%w(connection query result schema cube).each do |file|
|
13
|
+
require "mondrian/olap/#{file}"
|
14
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Mondrian
|
2
|
+
module OLAP
|
3
|
+
class Connection
|
4
|
+
def self.create(params)
|
5
|
+
connection = new(params)
|
6
|
+
connection.connect
|
7
|
+
connection
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :raw_connection, :raw_schema, :raw_schema_reader
|
11
|
+
|
12
|
+
def initialize(params={})
|
13
|
+
@params = params
|
14
|
+
@driver = params[:driver]
|
15
|
+
@connected = false
|
16
|
+
@raw_connection = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def connect
|
20
|
+
@raw_jdbc_connection = Java::JavaSql::DriverManager.getConnection(connection_string)
|
21
|
+
@raw_connection = @raw_jdbc_connection.unwrap(Java::OrgOlap4j::OlapConnection.java_class)
|
22
|
+
@raw_schema = @raw_connection.getSchema
|
23
|
+
@connected = true
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def connected?
|
28
|
+
@connected
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@raw_connection.close
|
33
|
+
@connected = false
|
34
|
+
@raw_connection = @raw_jdbc_connection = nil
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute(query_string)
|
39
|
+
statement = @raw_connection.prepareOlapStatement(query_string)
|
40
|
+
Result.new(self, statement.executeQuery())
|
41
|
+
end
|
42
|
+
|
43
|
+
def from(cube_name)
|
44
|
+
Query.from(self, cube_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def cube_names
|
48
|
+
@raw_schema.getCubes.map{|c| c.getName}
|
49
|
+
end
|
50
|
+
|
51
|
+
def cube(name)
|
52
|
+
Cube.get(self, name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Will affect only the next created connection. If it is necessary to clear all schema cache then
|
56
|
+
# flush_schema_cache should be called, then close and then new connection should be created.
|
57
|
+
def flush_schema_cache
|
58
|
+
unwrapped_connection = @raw_connection.unwrap(Java::MondrianOlap::Connection.java_class)
|
59
|
+
raw_cache_control = unwrapped_connection.getCacheControl(nil)
|
60
|
+
raw_cache_control.flushSchemaCache
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def connection_string
|
66
|
+
"jdbc:mondrian:Jdbc=#{jdbc_uri};JdbcDrivers=#{jdbc_driver};" <<
|
67
|
+
(@params[:catalog] ? "Catalog=#{catalog_uri}" : "CatalogContent=#{catalog_content}")
|
68
|
+
end
|
69
|
+
|
70
|
+
def jdbc_uri
|
71
|
+
case @driver
|
72
|
+
when 'mysql', 'postgresql'
|
73
|
+
"jdbc:#{@driver}://#{@params[:host]}#{@params[:port] && ":#{@params[:port]}"}/#{@params[:database]}" <<
|
74
|
+
"?user=#{@params[:username]}&password=#{@params[:password]}"
|
75
|
+
when 'oracle'
|
76
|
+
# connection using TNS alias
|
77
|
+
if @params[:database] && !@params[:host] && !@params[:url] && ENV['TNS_ADMIN']
|
78
|
+
"jdbc:oracle:thin:#{@params[:username]}/#{@params[:password]}@#{@params[:database]}"
|
79
|
+
else
|
80
|
+
@params[:url] ||
|
81
|
+
"jdbc:oracle:thin:#{@params[:username]}/#{@params[:password]}" <<
|
82
|
+
"@#{@params[:host] || 'localhost'}:#{@params[:port] || 1521}:#{@params[:database]}"
|
83
|
+
end
|
84
|
+
else
|
85
|
+
raise ArgumentError, 'unknown JDBC driver'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def jdbc_driver
|
90
|
+
case @driver
|
91
|
+
when 'mysql'
|
92
|
+
'com.mysql.jdbc.Driver'
|
93
|
+
when 'postgresql'
|
94
|
+
'org.postgresql.Driver'
|
95
|
+
when 'oracle'
|
96
|
+
'oracle.jdbc.OracleDriver'
|
97
|
+
else
|
98
|
+
raise ArgumentError, 'unknown JDBC driver'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def catalog_uri
|
103
|
+
if @params[:catalog]
|
104
|
+
"file://#{File.expand_path(@params[:catalog])}"
|
105
|
+
else
|
106
|
+
raise ArgumentError, 'missing catalog source'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def catalog_content
|
111
|
+
if @params[:catalog_content]
|
112
|
+
@params[:catalog_content]
|
113
|
+
elsif @params[:schema]
|
114
|
+
@params[:schema].to_xml(:driver => @driver)
|
115
|
+
else
|
116
|
+
raise ArgumentError, "Specify catalog with :catalog, :catalog_content or :schema option"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
module Mondrian
|
2
|
+
module OLAP
|
3
|
+
class Cube
|
4
|
+
def self.get(connection, name)
|
5
|
+
if raw_cube = connection.raw_schema.getCubes.get(name)
|
6
|
+
Cube.new(connection, raw_cube)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(connection, raw_cube)
|
11
|
+
@connection = connection
|
12
|
+
@raw_cube = raw_cube
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
@name ||= @raw_cube.getName
|
17
|
+
end
|
18
|
+
|
19
|
+
def dimensions
|
20
|
+
@dimenstions ||= @raw_cube.getDimensions.map{|d| Dimension.new(self, d)}
|
21
|
+
end
|
22
|
+
|
23
|
+
def dimension_names
|
24
|
+
dimensions.map{|d| d.name}
|
25
|
+
end
|
26
|
+
|
27
|
+
def dimension(name)
|
28
|
+
dimensions.detect{|d| d.name == name}
|
29
|
+
end
|
30
|
+
|
31
|
+
def query
|
32
|
+
Query.from(@connection, name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def member(full_name)
|
36
|
+
segment_names = Java::OrgOlap4jMdx::IdentifierNode.parseIdentifier(full_name).map do |segment|
|
37
|
+
segment.getName
|
38
|
+
end
|
39
|
+
member_by_segments(*segment_names)
|
40
|
+
end
|
41
|
+
|
42
|
+
def member_by_segments(*segment_names)
|
43
|
+
raw_member = @raw_cube.lookupMember(*segment_names)
|
44
|
+
raw_member && Member.new(raw_member)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Dimension
|
49
|
+
def initialize(cube, raw_dimension)
|
50
|
+
@cube = cube
|
51
|
+
@raw_dimension = raw_dimension
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :cube
|
55
|
+
|
56
|
+
def name
|
57
|
+
@name ||= @raw_dimension.getName
|
58
|
+
end
|
59
|
+
|
60
|
+
def full_name
|
61
|
+
@full_name ||= @raw_dimension.getUniqueName
|
62
|
+
end
|
63
|
+
|
64
|
+
def hierarchies
|
65
|
+
@hierarchies ||= @raw_dimension.getHierarchies.map{|h| Hierarchy.new(self, h)}
|
66
|
+
end
|
67
|
+
|
68
|
+
def hierarchy_names
|
69
|
+
hierarchies.map{|h| h.name}
|
70
|
+
end
|
71
|
+
|
72
|
+
def hierarchy(name = nil)
|
73
|
+
name ||= self.name
|
74
|
+
hierarchies.detect{|h| h.name == name}
|
75
|
+
end
|
76
|
+
|
77
|
+
def measures?
|
78
|
+
@raw_dimension.getDimensionType == Java::OrgOlap4jMetadata::Dimension::Type::MEASURE
|
79
|
+
end
|
80
|
+
|
81
|
+
def dimension_type
|
82
|
+
case @raw_dimension.getDimensionType
|
83
|
+
when Java::OrgOlap4jMetadata::Dimension::Type::TIME
|
84
|
+
:time
|
85
|
+
when Java::OrgOlap4jMetadata::Dimension::Type::MEASURE
|
86
|
+
:measures
|
87
|
+
else
|
88
|
+
:standard
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Hierarchy
|
94
|
+
def initialize(dimension, raw_hierarchy)
|
95
|
+
@dimension = dimension
|
96
|
+
@raw_hierarchy = raw_hierarchy
|
97
|
+
end
|
98
|
+
|
99
|
+
def name
|
100
|
+
@name ||= @raw_hierarchy.getName
|
101
|
+
end
|
102
|
+
|
103
|
+
def levels
|
104
|
+
@levels = @raw_hierarchy.getLevels.map{|l| Level.new(self, l)}
|
105
|
+
end
|
106
|
+
|
107
|
+
def level(name)
|
108
|
+
levels.detect{|l| l.name == name}
|
109
|
+
end
|
110
|
+
|
111
|
+
def level_names
|
112
|
+
levels.map{|l| l.name}
|
113
|
+
end
|
114
|
+
|
115
|
+
def has_all?
|
116
|
+
@raw_hierarchy.hasAll
|
117
|
+
end
|
118
|
+
|
119
|
+
def all_member_name
|
120
|
+
has_all? ? @raw_hierarchy.getRootMembers.first.getName : nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def all_member
|
124
|
+
has_all? ? Member.new(@raw_hierarchy.getRootMembers.first) : nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def root_members
|
128
|
+
@raw_hierarchy.getRootMembers.map{|m| Member.new(m)}
|
129
|
+
end
|
130
|
+
|
131
|
+
def root_member_names
|
132
|
+
@raw_hierarchy.getRootMembers.map{|m| m.getName}
|
133
|
+
end
|
134
|
+
|
135
|
+
def root_member_full_names
|
136
|
+
@raw_hierarchy.getRootMembers.map{|m| m.getUniqueName}
|
137
|
+
end
|
138
|
+
|
139
|
+
def child_names(*parent_member_segment_names)
|
140
|
+
parent_member = if parent_member_segment_names.empty?
|
141
|
+
return root_member_names unless has_all?
|
142
|
+
all_member
|
143
|
+
else
|
144
|
+
@dimension.cube.member_by_segments(*parent_member_segment_names)
|
145
|
+
end
|
146
|
+
parent_member && parent_member.children.map{|m| m.name}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class Level
|
151
|
+
def initialize(hierarchy, raw_level)
|
152
|
+
@hierarchy = hierarchy
|
153
|
+
@raw_level = raw_level
|
154
|
+
end
|
155
|
+
|
156
|
+
def name
|
157
|
+
@name ||= @raw_level.getName
|
158
|
+
end
|
159
|
+
|
160
|
+
def depth
|
161
|
+
@raw_level.getDepth
|
162
|
+
end
|
163
|
+
|
164
|
+
def cardinality
|
165
|
+
@cardinality = @raw_level.getCardinality
|
166
|
+
end
|
167
|
+
|
168
|
+
def members_count
|
169
|
+
@members_count ||= begin
|
170
|
+
if cardinality >= 0
|
171
|
+
cardinality
|
172
|
+
else
|
173
|
+
@raw_level.getMembers.size
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def members
|
179
|
+
@raw_level.getMembers.map{|m| Member.new(m)}
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class Member
|
184
|
+
def initialize(raw_member)
|
185
|
+
@raw_member = raw_member
|
186
|
+
end
|
187
|
+
|
188
|
+
def name
|
189
|
+
@raw_member.getName
|
190
|
+
end
|
191
|
+
|
192
|
+
def full_name
|
193
|
+
@raw_member.getUniqueName
|
194
|
+
end
|
195
|
+
|
196
|
+
def calculated?
|
197
|
+
@raw_member.isCalculated
|
198
|
+
end
|
199
|
+
|
200
|
+
def drillable?
|
201
|
+
return false if calculated?
|
202
|
+
# @raw_member.getChildMemberCount > 0
|
203
|
+
# This hopefully is faster than counting actual child members
|
204
|
+
raw_level = @raw_member.getLevel
|
205
|
+
raw_levels = raw_level.getHierarchy.getLevels
|
206
|
+
raw_levels.indexOf(raw_level) < raw_levels.size - 1
|
207
|
+
end
|
208
|
+
|
209
|
+
def depth
|
210
|
+
@raw_member.getDepth
|
211
|
+
end
|
212
|
+
|
213
|
+
def children
|
214
|
+
@raw_member.getChildMembers.map{|m| Member.new(m)}
|
215
|
+
end
|
216
|
+
|
217
|
+
def descendants_at_level(level)
|
218
|
+
raw_level = @raw_member.getLevel
|
219
|
+
raw_levels = raw_level.getHierarchy.getLevels
|
220
|
+
current_level_index = raw_levels.indexOf(raw_level)
|
221
|
+
descendants_level_index = raw_levels.indexOfName(level)
|
222
|
+
|
223
|
+
return nil unless descendants_level_index > current_level_index
|
224
|
+
|
225
|
+
members = [self]
|
226
|
+
(descendants_level_index - current_level_index).times do
|
227
|
+
members = members.map do |member|
|
228
|
+
member.children
|
229
|
+
end.flatten
|
230
|
+
end
|
231
|
+
members
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|