olap-xmla 0.0.1 → 0.0.2

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