olap-xmla 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0b8dba43fb5369997ffdd603813f84560764468c
4
- data.tar.gz: a05013ba124097d44460b0975e374dc371efa17e
3
+ metadata.gz: 353d1787b60fcbfdb612748987277c3d3b4046ea
4
+ data.tar.gz: b7e723cf2067e094e4ba74e51267b3b3c2cd2048
5
5
  SHA512:
6
- metadata.gz: d8cb6e2cf34c2c8723b7f12f53540e3932513577273b638dfdcf64b751544602efbedd0e0f77d161782d79fa2fe65fc82091d38554558ae23cd017d5632b6d16
7
- data.tar.gz: b85dd1a91398b39191068c9356b146dfd5bf2078e97750d4226a8cb2f958f9ce3cbf419f12056fc4882e26dc7f036c7c0ef28b368a764e88126c8d565b0e3f05
6
+ metadata.gz: 9558ccbf3c6d913a7074ea34b6bb722bc5b75983e0dff1890ce98bbd45f2f1d000ada9ce790493988d8da66993439ff8dedd8062425d07f1e295954e0312a896
7
+ data.tar.gz: ddafa507a59edd7a7cab7035c5389672d9b156594b0550726b5ef36894710500407eb93757b536d42590a128ede68a018668a4383ac605263af5f36bb6988c5d
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
+ /spec/.config.yaml
9
10
  /tmp/
10
11
  *.bundle
11
12
  *.so
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in olap-xmla.gemspec
4
4
  gemspec
5
+
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Olap::Xmla
2
2
 
3
- TODO: Write a gem description
3
+ The gem connects to OLAP database using XMLA interface and executes MDX queries.
4
+
5
+ Can be used in Ruby or Rails applications to display and analyse data from OLAP databases.
4
6
 
5
7
  ## Installation
6
8
 
@@ -20,7 +22,82 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
23
- TODO: Write usage instructions here
25
+ Using the gem is very simple, only basic knowledge on OLAP is required.
26
+
27
+ ### Connecting to server
28
+
29
+ To use the gem, you need to know the connection requisites to connect to XMLA server:
30
+
31
+ 1. Server URL ( typically, http: or https: URL )
32
+ 2. Datasource and catalog names. You can check them in your XMLA server configuration
33
+
34
+ Connecting to the server and executing MDX are straightforward:
35
+
36
+ ```ruby
37
+ require 'olap/xmla'
38
+
39
+ client = Olap::Xmla.client({
40
+ server: 'http://your-olap-server',
41
+ datasource: 'your-datasource',
42
+ catalog: 'your-catalog'})
43
+ response = client.request 'your-mdx-here'
44
+ ```
45
+
46
+ ### Configuration in Rails
47
+
48
+ If you are using this gem in Rails application, which uses just single OLAP data source,
49
+ you can simplify the code by pre-configuring the XMLA connection.
50
+
51
+ Create a file olap.rb in config/initializers directory with the following content:
52
+
53
+ ```ruby
54
+ Olap::Xmla.default_options= { server: 'http://your-olap-server',
55
+ datasource: 'your-datasource',
56
+ catalog: 'your-catalog'}
57
+ ```
58
+
59
+ Then in Rails application code you can simply do:
60
+
61
+ ```ruby
62
+ response = Olap::Xmla.client.request 'your-mdx-here'
63
+ ```
64
+
65
+ ### Querying MDX
66
+
67
+ The gem does not parse MDX, just passes it to XMLA server.
68
+
69
+ However, it can do substituting parameters in the query:
70
+
71
+ ```ruby
72
+ MDX_QUERY = 'SET [~ROWS_Date] AS {[DateTime].[Date].[Date].[%DATE%]}'
73
+
74
+ Olap::Xmla.client.request MDX_QUERY, {'%DATE%' => '20150530'}
75
+ ```
76
+
77
+ This allows to store MDX queries in constants, while execute them with dynamic parameters.
78
+ Note, that you should never use these parameters directly from Rails request, as
79
+ this may create security breach!
80
+
81
+ ### Using response on Query
82
+
83
+
84
+ You may use the response to render the results to user, or post-process it to analyse the data
85
+ The following methods can be used to request the meta-data and data from the response:
86
+
87
+ ```ruby
88
+ response = client.request(mdx)
89
+
90
+
91
+ # Meta - data of the response
92
+ response.measures # array of the columns definitions ( :name / :caption )
93
+ response.dimensions # array of the rows definitions ( :name )
94
+
95
+ # Response data
96
+ response.rows # rows of the response
97
+ response.to_hash # response as a hash
98
+ response.column_values(column_num) # just one column of the response
99
+
100
+ ```
24
101
 
25
102
  ## Contributing
26
103
 
data/lib/olap/xmla.rb CHANGED
@@ -1,10 +1,50 @@
1
1
  require "olap/xmla/version"
2
+ require "olap/xmla/client"
2
3
 
3
4
  module Olap
5
+
6
+ #
7
+ # Connects to XMLA[https://en.wikipedia.org/wiki/XML_for_Analysis] server and executes MDX[https://en.wikipedia.org/wiki/MultiDimensional_eXpressions] queries
8
+ #
4
9
  module Xmla
5
10
 
6
- def self.client server, data_source, catalog, options = {}
7
- Olap::Xmla::Client.new server, data_source, catalog, options
11
+ @@connect_options = {}
12
+
13
+ # Configure the default options to connect to XMLA server
14
+ # Can be optionally used to setup connection options in one place in application,
15
+ #
16
+ # Example:
17
+ # >> Olap::Xmla.default_options = {server: 'http://your-olap-server', datasource: 'your-datasource', catalog: 'your-catalog'}
18
+ # >> Olap::Xmla.client.request mdx
19
+ # => #<Olap::Xmla::Response:0x000001035b9510 @response={ ...
20
+ #
21
+ # Look client connect_options for the list of options to be specified
22
+ #
23
+ def self.default_options= options
24
+ @@connect_options = options
25
+ end
26
+
27
+ # Create a client, which can be used then to execute MDX queries
28
+ #
29
+ # Example:
30
+ # >> client = Olap::Xmla.client(server: 'http://your-olap-server', datasource: 'your-datasource', catalog: 'your-catalog')
31
+ # >> response = client.request mdx
32
+ # => #<Olap::Xmla::Response:0x000001035b9510 @response={ ...
33
+ #
34
+ # ==== connect_options
35
+ #
36
+ # * +:server+ - URL to connect to XMLA server (required)
37
+ # * +:datasource+ - the name of datasource (required)
38
+ # * +:catalog+ - the name of catalog (required)
39
+ # * +:open_timeout+ - open timeout to connect to XMLA server, optional, default is 60 sec
40
+ # * +:read_timeout+ - open timeout to read data from XMLA server, optional, default is 300 sec
41
+ #
42
+ #
43
+ def self.client connect_options = {}
44
+ options = @@connect_options.merge connect_options
45
+ raise "Connect options must define :server, :datasource and :catalog options" unless
46
+ options[:server] && options[:datasource] && options[:catalog]
47
+ Olap::Xmla::Client.new options[:server], options[:datasource], options[:catalog], options
8
48
  end
9
49
 
10
50
  end
@@ -0,0 +1,74 @@
1
+ require 'savon'
2
+ require 'olap/xmla/response'
3
+
4
+ class Olap::Xmla::Client
5
+
6
+ attr_reader :data_source, :catalog, :client
7
+
8
+ def initialize server, data_source, catalog, options
9
+
10
+ @catalog = catalog
11
+ @data_source = data_source
12
+ @client = Savon.client do
13
+ endpoint server
14
+ namespace "urn:schemas-microsoft-com:xml-analysis"
15
+ open_timeout (options[:open_timeout] || 60)
16
+ read_timeout (options[:read_timeout] || 300)
17
+ end
18
+ end
19
+
20
+ # Executes multiple MDX queries as a batch
21
+ #
22
+ # Arguments:
23
+ # * +mdx_requests+ - Collection of MDX requests
24
+ # * +parameters+ - Map of parameters to substitute in MDX request, optional
25
+ #
26
+ def batch mdx_requests, parameters = {}
27
+ mdx_requests.collect{|mdx|
28
+ request mdx, parameters
29
+ }
30
+ end
31
+
32
+ # Execute MDX queries, substituting parameters in the query
33
+ #
34
+ # Arguments:
35
+ # * +mdx_request+ - MDX request as a string, required
36
+ # * +parameters+ - Map of parameters to substitute in MDX request, optional
37
+ #
38
+ # Example:
39
+ #
40
+ # client.request 'SET [~ROWS_Date] AS {[DateTime].[Date].[Date].[%DATE%]}', {'%DATE%' => '20150530'}
41
+ # will execute actual MDX: SET [~ROWS_Date] AS {[DateTime].[Date].[Date].[20150530]}
42
+ #
43
+ #
44
+ def request mdx_request, parameters = {}
45
+
46
+ mdx = mdx_request.clone
47
+
48
+ parameters.each{|k,v|
49
+ mdx.gsub!(k,v)
50
+ }
51
+
52
+ ops = client.operation('Execute')
53
+ p = { 'wsdl:PropertyList' => {'wsdl:DataSourceInfo' => data_source,
54
+ 'wsdl:Catalog' => catalog,
55
+ 'wsdl:Format' => 'Multidimensional',
56
+ 'wsdl:AxisFormat'=> 'TupleFormat'} }
57
+
58
+
59
+ r = ops.call( message: {'wsdl:Command' => { 'wsdl:Statement' => mdx}, 'wsdl:Properties' => p })
60
+
61
+ unless r.success?
62
+ raise "Error executing #{mdx} in #{catalog} #{data_source}: #{r.http_error} #{r.soap_fault}"
63
+ end
64
+
65
+ Olap::Xmla::Response.new r.body[:execute_response][:return][:root]
66
+
67
+ end
68
+
69
+
70
+
71
+ end
72
+
73
+
74
+
@@ -0,0 +1,154 @@
1
+ class Olap::Xmla::Response
2
+
3
+ attr_reader :response
4
+
5
+ def initialize response
6
+ @response = response
7
+ end
8
+
9
+ # Returns true if the response has any data
10
+ def has_data?
11
+ not response[:cell_data][:cell].empty?
12
+ end
13
+
14
+ # Collection of measures in response
15
+ #
16
+ # * +:name+ the name of measure
17
+ # * +:caption+ display name of measure
18
+ #
19
+ def measures
20
+ response[:axes][:axis][0][:tuples][:tuple].collect{|m|
21
+ {
22
+ name: m[:member][:u_name],
23
+ caption: m[:member][:caption]
24
+ }
25
+ }
26
+ end
27
+
28
+ # Collection of dimensions in response
29
+ #
30
+ # * +:name+ the name of dimension
31
+ #
32
+ def dimensions
33
+ response[:olap_info][:axes_info][:axis_info][1][:hierarchy_info].collect{|m|
34
+ {
35
+ name: m[:@name]
36
+ }
37
+ }
38
+ end
39
+
40
+
41
+ # Collection of result rows
42
+ # * rownum number of row ( 1... N)
43
+ # * labels - array of tuples per the row
44
+ # value - the value of tuple
45
+ # name - the name of tuple
46
+ # * values - the values array
47
+ # measure - metric name
48
+ # value - metric value
49
+ # fmt_value - formatted metric value
50
+ # colnum - column number ( 1..N)
51
+ def rows
52
+
53
+ return [] unless response[:cell_data]
54
+
55
+ measures = [response[:axes][:axis][0][:tuples][:tuple]].flatten
56
+ cells = [response[:cell_data][:cell]].flatten
57
+
58
+ cell_ordinal = 0
59
+ cell_index = 0
60
+ rownum = 0
61
+
62
+ [response[:axes][:axis][1][:tuples][:tuple]].flatten.collect{ |tuple|
63
+ rownum += 1
64
+ colnum = 0
65
+ tuple_member = [tuple[:member]].flatten
66
+ { rownum: rownum,
67
+ labels: tuple_member.collect{|member|
68
+ value = member[:caption]
69
+ value = nil if value=='#null'
70
+ {value: value, name: member[:@hierarchy]}
71
+ },
72
+ values: measures.collect{|m|
73
+
74
+ colnum += 1
75
+
76
+ if (cell=cells[cell_index]) && cell[:@cell_ordinal].to_i==cell_ordinal
77
+ cell_index += 1
78
+ cell_ordinal += 1
79
+ { colnum: colnum, measure: m[:member][:u_name], value: cell[:value], fmt_value: cell[:fmt_value]}
80
+ else
81
+ cell_ordinal += 1
82
+ { colnum: colnum, measure: m[:member][:u_name], value: nil, fmt_value: nil}
83
+ end
84
+
85
+ }
86
+
87
+ }
88
+ }
89
+
90
+ end
91
+
92
+ # query a colum of the result
93
+ #
94
+ # Example:
95
+ #
96
+ # >> response.column_values 1
97
+ # => [30.0, 1025.0, 884.0, 543.0,...
98
+ #
99
+ def column_values column_num
100
+
101
+ rows.collect{|row|
102
+ row[:values].detect{|value|
103
+ value[:colnum]==column_num
104
+ }[:value].to_f
105
+ }
106
+
107
+ end
108
+
109
+ # Aggregate result by one of the columns
110
+ def column_values_aggregate dimension_aggr_index = 0
111
+
112
+ result = []
113
+ index = {}
114
+
115
+ rows.each{|row|
116
+ label = row[:labels][dimension_aggr_index]
117
+
118
+ if i = index[label]
119
+ for j in 0..result[i][:values].count-1
120
+ result[i][:values][j] += row[:values][j][:value].to_f
121
+ end
122
+ else
123
+ index[label] = result.count
124
+ result << {
125
+ rownum: result.count + 1,
126
+ label: label,
127
+ values: row[:values].collect{|v| v[:value].to_f }
128
+ }
129
+ end
130
+ }
131
+
132
+ result
133
+ end
134
+
135
+ # Convert the response to hash
136
+ # keys of hash are values of tuples
137
+ # values of hash are values of metrics
138
+ #
139
+ # Example:
140
+ #
141
+ # response.to_hash
142
+ # => {["2014", "05/11/2014"]=>[30.0, 27.0, 0.0], ["2014", "06/11/2014"]=>[1025.0, 688.0, 73.0]
143
+ #
144
+ def to_hash
145
+ Hash[rows.collect{|row|
146
+ [
147
+ row[:labels].collect{|l| l[:value].nil? ? nil : l[:value] },
148
+ row[:values].collect{|l| (l.nil? || l[:value].nil?) ? 0.0 : l[:value].to_f },
149
+ ]
150
+
151
+ }]
152
+ end
153
+
154
+ end
@@ -1,5 +1,5 @@
1
1
  module Olap
2
2
  module Xmla
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
data/olap-xmla.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["aleksey@wondersoft.ru"]
11
11
  spec.summary = %q{Pure Ruby gem to make MDX queries on OLAP databases using XMLA connection}
12
12
  spec.description = %q{Can be used with any XMLA-compliant server, like Olaper or Mondrian.}
13
- spec.homepage = ""
13
+ spec.homepage = "https://github.com/Wondersoft/olap-xmla"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
@@ -20,4 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.7"
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", '~> 0'
24
+ spec.add_runtime_dependency 'savon', '~> 2.8'
25
+ spec.add_runtime_dependency 'rubyntlm', '~> 0.3.2'
23
26
  end
data/spec/gem_spec.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ connection_config = YAML.load File.read 'spec/.config.yaml'
5
+
6
+ describe 'Test MDX queries' do
7
+
8
+ it 'should request mdx' do
9
+
10
+ client = Olap::Xmla.client connection_config
11
+
12
+ mdx= "WITH SET [~ROWS_YearByDay] AS " +
13
+ "{[DateTime].[YearByDay].[Year].Members} " +
14
+ "SET [~ROWS_Date] AS " +
15
+ " {[DateTime].[Date].[Date].Members} " +
16
+ "SELECT " +
17
+ "NON EMPTY {[Measures].[Slots], [Measures].[Clicks], [Measures].[Conversions]} ON COLUMNS, " +
18
+ "NON EMPTY NonEmptyCrossJoin([~ROWS_YearByDay], [~ROWS_Date]) ON ROWS " +
19
+ "FROM [Ad Serving]"
20
+
21
+ response = client.request mdx
22
+
23
+ expect(response.has_data?).to be true
24
+
25
+ expect(response.measures.count).to eq 3
26
+ expect(response.measures[0][:name]).to eq '[Measures].[Slots]'
27
+ expect(response.measures[1][:name]).to eq '[Measures].[Clicks]'
28
+
29
+ expect(response.dimensions.count).to eq 2
30
+
31
+ expect(response.dimensions[0][:name]).to eq 'YearByDay'
32
+ expect(response.dimensions[1][:name]).to eq 'Date'
33
+
34
+ expect(response.rows.count).to be > 100
35
+
36
+ expect(response.rows[0][:rownum]).to eq 1
37
+
38
+ expect(response.rows[0][:labels].size).to eq 2
39
+ expect(response.rows[0][:labels][0][:name]).to eq 'YearByDay'
40
+ expect(response.rows[0][:labels][1][:name]).to eq 'Date'
41
+
42
+ expect(response.rows[0][:values].size).to eq 3
43
+
44
+ expect(response.rows[0][:values][0][:colnum]).to eq 1
45
+ expect(response.rows[0][:values][0][:measure]).to eq '[Measures].[Slots]'
46
+ expect(response.rows[0][:values][0][:value]).to_not be_nil
47
+
48
+ end
49
+
50
+ it 'should use default connect' do
51
+
52
+ Olap::Xmla.default_options = connection_config
53
+
54
+ client = Olap::Xmla.client
55
+
56
+ mdx= "WITH SET [~ROWS_YearByDay] AS " +
57
+ "{[DateTime].[YearByDay].[Year].Members} " +
58
+ "SET [~ROWS_Date] AS " +
59
+ " {[DateTime].[Date].[Date].Members} " +
60
+ "SELECT " +
61
+ "NON EMPTY {[Measures].[Slots], [Measures].[Clicks], [Measures].[Conversions]} ON COLUMNS, " +
62
+ "NON EMPTY NonEmptyCrossJoin([~ROWS_YearByDay], [~ROWS_Date]) ON ROWS " +
63
+ "FROM [Ad Serving]"
64
+
65
+ response = client.request mdx
66
+
67
+ expect(response.has_data?).to be true
68
+
69
+ end
70
+
71
+
72
+ end
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'yaml'
4
+
5
+ Bundler.setup
6
+
7
+ require 'olap/xmla'
8
+
9
+
10
+ RSpec.configure do |config|
11
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: olap-xmla
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - studnev
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: savon
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubyntlm
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.3.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.3.2
41
83
  description: Can be used with any XMLA-compliant server, like Olaper or Mondrian.
42
84
  email:
43
85
  - aleksey@wondersoft.ru
@@ -51,9 +93,13 @@ files:
51
93
  - README.md
52
94
  - Rakefile
53
95
  - lib/olap/xmla.rb
96
+ - lib/olap/xmla/client.rb
97
+ - lib/olap/xmla/response.rb
54
98
  - lib/olap/xmla/version.rb
55
99
  - olap-xmla.gemspec
56
- homepage: ''
100
+ - spec/gem_spec.rb
101
+ - spec/spec_helper.rb
102
+ homepage: https://github.com/Wondersoft/olap-xmla
57
103
  licenses:
58
104
  - MIT
59
105
  metadata: {}
@@ -77,4 +123,6 @@ rubygems_version: 2.2.2
77
123
  signing_key:
78
124
  specification_version: 4
79
125
  summary: Pure Ruby gem to make MDX queries on OLAP databases using XMLA connection
80
- test_files: []
126
+ test_files:
127
+ - spec/gem_spec.rb
128
+ - spec/spec_helper.rb