ruby-druid 0.1.9 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +18 -16
- data/README.md +78 -111
- data/lib/druid.rb +0 -6
- data/lib/druid/aggregation.rb +66 -0
- data/lib/druid/client.rb +10 -82
- data/lib/druid/context.rb +37 -0
- data/lib/druid/data_source.rb +95 -0
- data/lib/druid/filter.rb +228 -172
- data/lib/druid/granularity.rb +39 -0
- data/lib/druid/having.rb +149 -29
- data/lib/druid/post_aggregation.rb +191 -77
- data/lib/druid/query.rb +422 -156
- data/lib/druid/version.rb +3 -0
- data/lib/druid/zk.rb +141 -0
- data/ruby-druid.gemspec +24 -12
- data/spec/lib/client_spec.rb +14 -61
- data/spec/lib/data_source_spec.rb +65 -0
- data/spec/lib/query_spec.rb +359 -250
- data/spec/lib/{zoo_handler_spec.rb → zk_spec.rb} +51 -66
- metadata +142 -34
- data/.gitignore +0 -6
- data/.rspec +0 -2
- data/.travis.yml +0 -9
- data/Gemfile +0 -12
- data/Rakefile +0 -2
- data/bin/dripl +0 -38
- data/dot_driplrc_example +0 -12
- data/lib/druid/console.rb +0 -74
- data/lib/druid/response_row.rb +0 -32
- data/lib/druid/serializable.rb +0 -19
- data/lib/druid/zoo_handler.rb +0 -129
data/lib/druid/zk.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'zk'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'rest_client'
|
4
|
+
|
5
|
+
module Druid
|
6
|
+
class ZK
|
7
|
+
def initialize(uri, opts = {})
|
8
|
+
@zk = ::ZK.new(uri, chroot: :check)
|
9
|
+
@registry = Hash.new { |hash, key| hash[key] = Array.new }
|
10
|
+
@discovery_path = opts[:discovery_path] || '/discovery'
|
11
|
+
@watched_services = Hash.new
|
12
|
+
register
|
13
|
+
end
|
14
|
+
|
15
|
+
def register
|
16
|
+
$log.info("druid.zk register discovery path") if $log
|
17
|
+
@zk.on_expired_session { register }
|
18
|
+
@zk.register(@discovery_path, only: :child) do |event|
|
19
|
+
$log.info("druid.zk got event on discovery path") if $log
|
20
|
+
check_services
|
21
|
+
end
|
22
|
+
check_services
|
23
|
+
end
|
24
|
+
|
25
|
+
def close!
|
26
|
+
$log.info("druid.zk shutting down") if $log
|
27
|
+
@zk.close!
|
28
|
+
end
|
29
|
+
|
30
|
+
def register_service(service, brokers)
|
31
|
+
$log.info("druid.zk register", service: service, brokers: brokers) if $log
|
32
|
+
# poor mans load balancing
|
33
|
+
@registry[service] = brokers.shuffle
|
34
|
+
end
|
35
|
+
|
36
|
+
def unregister_service(service)
|
37
|
+
$log.info("druid.zk unregister", service: service) if $log
|
38
|
+
@registry.delete(service)
|
39
|
+
unwatch_service(service)
|
40
|
+
end
|
41
|
+
|
42
|
+
def watch_service(service)
|
43
|
+
return if @watched_services.include?(service)
|
44
|
+
$log.info("druid.zk watch", service: service) if $log
|
45
|
+
watch = @zk.register(watch_path(service), only: :child) do |event|
|
46
|
+
$log.info("druid.zk got event on watch path for", service: service, event: event) if $log
|
47
|
+
unwatch_service(service)
|
48
|
+
check_service(service)
|
49
|
+
end
|
50
|
+
@watched_services[service] = watch
|
51
|
+
end
|
52
|
+
|
53
|
+
def unwatch_service(service)
|
54
|
+
return unless @watched_services.include?(service)
|
55
|
+
$log.info("druid.zk unwatch", service: service) if $log
|
56
|
+
@watched_services.delete(service).unregister
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_services
|
60
|
+
$log.info("druid.zk checking services") if $log
|
61
|
+
zk_services = @zk.children(@discovery_path, watch: true)
|
62
|
+
|
63
|
+
(services - zk_services).each do |service|
|
64
|
+
unregister_service(service)
|
65
|
+
end
|
66
|
+
|
67
|
+
zk_services.each do |service|
|
68
|
+
check_service(service)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def verify_broker(service, name)
|
73
|
+
$log.info("druid.zk verify", broker: name, service: service) if $log
|
74
|
+
info = @zk.get("#{watch_path(service)}/#{name}")
|
75
|
+
node = MultiJson.load(info[0])
|
76
|
+
uri = "http://#{node['address']}:#{node['port']}/druid/v2/"
|
77
|
+
check = RestClient::Request.execute({
|
78
|
+
method: :get, url: "#{uri}datasources/",
|
79
|
+
timeout: 5, open_timeout: 5
|
80
|
+
})
|
81
|
+
$log.info("druid.zk verified", uri: uri, sources: check) if $log
|
82
|
+
return [uri, MultiJson.load(check.to_str)] if check.code == 200
|
83
|
+
rescue
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
def watch_path(service)
|
88
|
+
"#{@discovery_path}/#{service}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def check_service(service)
|
92
|
+
return if @watched_services.include?(service)
|
93
|
+
|
94
|
+
watch_service(service)
|
95
|
+
|
96
|
+
known = @registry[service].map { |node| node[:name] }
|
97
|
+
live = @zk.children(watch_path(service), watch: true)
|
98
|
+
new_list = @registry[service].select { |node| live.include?(node[:name]) }
|
99
|
+
$log.info("druid.zk checking", service: service, known: known, live: live, new_list: new_list) if $log
|
100
|
+
|
101
|
+
# verify the new entries to be living brokers
|
102
|
+
(live - known).each do |name|
|
103
|
+
uri, sources = verify_broker(service, name)
|
104
|
+
new_list.push({ name: name, uri: uri, data_sources: sources }) if uri
|
105
|
+
end
|
106
|
+
|
107
|
+
if new_list.empty?
|
108
|
+
# don't show services w/o active brokers
|
109
|
+
unregister_service(service)
|
110
|
+
else
|
111
|
+
register_service(service, new_list)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def services
|
116
|
+
@registry.keys
|
117
|
+
end
|
118
|
+
|
119
|
+
def data_sources
|
120
|
+
result = Hash.new { |hash, key| hash[key] = [] }
|
121
|
+
|
122
|
+
@registry.each do |service, brokers|
|
123
|
+
brokers.each do |broker|
|
124
|
+
broker[:data_sources].each do |data_source|
|
125
|
+
result["#{service}/#{data_source}"] << broker[:uri]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
result.each do |source, uris|
|
131
|
+
result[source] = uris.sample if uris.respond_to?(:sample)
|
132
|
+
end
|
133
|
+
|
134
|
+
result
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
@registry.to_s
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/ruby-druid.gemspec
CHANGED
@@ -1,20 +1,32 @@
|
|
1
|
-
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$:.unshift(lib) unless $:.include?(lib)
|
3
|
+
|
4
|
+
require "druid/version"
|
2
5
|
|
3
6
|
Gem::Specification.new do |spec|
|
4
7
|
spec.name = "ruby-druid"
|
5
|
-
spec.version =
|
6
|
-
spec.authors = ["
|
7
|
-
spec.
|
8
|
-
spec.
|
9
|
-
|
10
|
-
|
8
|
+
spec.version = Druid::VERSION
|
9
|
+
spec.authors = ["Ruby Druid Community"]
|
10
|
+
spec.summary = %q{A Ruby client for Druid}
|
11
|
+
spec.description = <<-EOF
|
12
|
+
ruby-druid is a Ruby client for Druid. It includes a Squeel-like query DSL
|
13
|
+
and generates a JSON query that can be sent to Druid directly.
|
14
|
+
EOF
|
15
|
+
spec.homepage = "https://github.com/ruby-druid/ruby-druid"
|
11
16
|
spec.license = "MIT"
|
12
17
|
|
13
|
-
spec.files
|
14
|
-
spec.
|
15
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.files = Dir["lib/**/*"] + %w{LICENSE README.md ruby-druid.gemspec}
|
19
|
+
spec.test_files = Dir["spec/**/*"]
|
16
20
|
spec.require_paths = ["lib"]
|
17
21
|
|
18
|
-
spec.add_dependency "
|
19
|
-
spec.add_dependency "
|
22
|
+
spec.add_dependency "activesupport", "~> 4.2"
|
23
|
+
spec.add_dependency "activemodel", "~> 4.2"
|
24
|
+
spec.add_dependency "iso8601", "~> 0.9"
|
25
|
+
spec.add_dependency "multi_json", "~> 1.12"
|
26
|
+
spec.add_dependency "rest-client", "~> 2.0"
|
27
|
+
spec.add_dependency "zk", "~> 1.9"
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
29
|
+
spec.add_development_dependency "rake", "~> 11.2"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
31
|
+
spec.add_development_dependency "webmock", "~> 2.1"
|
20
32
|
end
|
data/spec/lib/client_spec.rb
CHANGED
@@ -1,67 +1,20 @@
|
|
1
1
|
describe Druid::Client do
|
2
2
|
|
3
3
|
it 'calls zookeeper on intialize' do
|
4
|
-
Druid::
|
5
|
-
Druid::Client.new('test_uri'
|
6
|
-
end
|
7
|
-
|
8
|
-
it '
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
group(:group1)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'parses response on 200' do
|
23
|
-
stub_request(:post, "http://www.example.com/druid/v2").
|
24
|
-
with(:body => "{\"dataSource\":\"test\",\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"]}",
|
25
|
-
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}).
|
26
|
-
to_return(:status => 200, :body => "[]", :headers => {})
|
27
|
-
Druid::ZooHandler.stub(:new).and_return(double(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com/druid/v2'}, :close! => true))
|
28
|
-
client = Druid::Client.new('test_uri', zk_keepalive: true)
|
29
|
-
JSON.should_receive(:parse).and_return([])
|
30
|
-
client.send(client.query('test/test').interval("2013-04-04", "2013-04-04"))
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'raises on request failure' do
|
34
|
-
stub_request(:post, "http://www.example.com/druid/v2").
|
35
|
-
with(:body => "{\"dataSource\":\"test\",\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"]}",
|
36
|
-
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}).
|
37
|
-
to_return(:status => 666, :body => "Strange server error", :headers => {})
|
38
|
-
Druid::ZooHandler.stub(:new).and_return(double(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com/druid/v2'}, :close! => true))
|
39
|
-
client = Druid::Client.new('test_uri', zk_keepalive: true)
|
40
|
-
expect { client.send(client.query('test/test').interval("2013-04-04", "2013-04-04")) }.to raise_error(RuntimeError, /Request failed: 666: Strange server error/)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'should have a static setup' do
|
44
|
-
client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'mock_uri'})
|
45
|
-
client.data_sources.should == ['madvertise/mock']
|
46
|
-
client.data_source_uri('madvertise/mock').should == URI('mock_uri')
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'should report dimensions of a data source correctly' do
|
50
|
-
stub_request(:get, "http://www.example.com/druid/v2/datasources/mock").
|
51
|
-
with(:headers =>{'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
|
52
|
-
to_return(:status => 200, :body => '{"dimensions":["d1","d2","d3"],"metrics":["m1", "m2"]}')
|
53
|
-
|
54
|
-
client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'http://www.example.com/druid/v2/'})
|
55
|
-
client.data_source('madvertise/mock').dimensions.should == ["d1","d2","d3"]
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'should report metrics of a data source correctly' do
|
59
|
-
stub_request(:get, "http://www.example.com/druid/v2/datasources/mock").
|
60
|
-
with(:headers =>{'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
|
61
|
-
to_return(:status => 200, :body => '{"dimensions":["d1","d2","d3"],"metrics":["m1", "m2"]}')
|
62
|
-
|
63
|
-
client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'http://www.example.com/druid/v2/'})
|
64
|
-
client.data_source('madvertise/mock').metrics.should == ["m1","m2"]
|
4
|
+
expect(Druid::ZK).to receive(:new).with('test_uri', {})
|
5
|
+
Druid::Client.new('test_uri')
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'returns the correct data source' do
|
9
|
+
stub_request(:get, "http://www.example.com/druid/v2/datasources/test").
|
10
|
+
with(:headers => { 'Accept'=>'*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent' => 'Ruby' }).
|
11
|
+
to_return(:status => 200, :body => "{\"dimensions\":[\"d1\", \"d2\"], \"metrics\":[\"m1\", \"m2\"]}", :headers => {})
|
12
|
+
expect(Druid::ZK).to receive(:new).and_return(double(Druid::ZK, :data_sources => { 'test/test' => 'http://www.example.com/druid/v2/' }, :close! => true))
|
13
|
+
client = Druid::Client.new('test_uri')
|
14
|
+
ds = client.data_source('test/test')
|
15
|
+
expect(ds.name).to eq('test')
|
16
|
+
expect(ds.metrics).to eq(['m1', 'm2'])
|
17
|
+
expect(ds.dimensions).to eq(['d1', 'd2'])
|
65
18
|
end
|
66
19
|
|
67
20
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
describe Druid::DataSource do
|
2
|
+
|
3
|
+
context '#post' do
|
4
|
+
it 'parses response on 200' do
|
5
|
+
# MRI
|
6
|
+
stub_request(:post, "http://www.example.com/druid/v2").
|
7
|
+
with(:body => "{\"context\":{\"queryId\":null},\"queryType\":\"timeseries\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"granularity\":\"all\",\"dataSource\":\"test\"}",
|
8
|
+
:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
|
9
|
+
to_return(:status => 200, :body => '[]', :headers => {})
|
10
|
+
# JRuby ... *sigh
|
11
|
+
stub_request(:post, "http://www.example.com/druid/v2").
|
12
|
+
with(:body => "{\"context\":{\"queryId\":null},\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"queryType\":\"timeseries\",\"dataSource\":\"test\"}",
|
13
|
+
:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
|
14
|
+
to_return(:status => 200, :body => '[]', :headers => {})
|
15
|
+
ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2')
|
16
|
+
query = Druid::Query::Builder.new.interval('2013-04-04', '2013-04-04').granularity(:all).query
|
17
|
+
query.context.queryId = nil
|
18
|
+
expect(ds.post(query)).to be_empty
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises on request failure' do
|
22
|
+
# MRI
|
23
|
+
stub_request(:post, 'http://www.example.com/druid/v2').
|
24
|
+
with(:body => "{\"context\":{\"queryId\":null},\"queryType\":\"timeseries\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"granularity\":\"all\",\"dataSource\":\"test\"}",
|
25
|
+
:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
|
26
|
+
to_return(:status => 666, :body => 'Strange server error', :headers => {})
|
27
|
+
# JRuby ... *sigh
|
28
|
+
stub_request(:post, 'http://www.example.com/druid/v2').
|
29
|
+
with(:body => "{\"context\":{\"queryId\":null},\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"],\"queryType\":\"timeseries\",\"dataSource\":\"test\"}",
|
30
|
+
:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type' => 'application/json', 'User-Agent' => 'Ruby' }).
|
31
|
+
to_return(:status => 666, :body => 'Strange server error', :headers => {})
|
32
|
+
ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2')
|
33
|
+
query = Druid::Query::Builder.new.interval('2013-04-04', '2013-04-04').granularity(:all).query
|
34
|
+
query.context.queryId = nil
|
35
|
+
expect { ds.post(query) }.to raise_error(Druid::DataSource::Error)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context '#metadata' do
|
40
|
+
it 'parses metrics on 200' do
|
41
|
+
stub_request(:get, 'http://www.example.com/druid/v2/datasources/test').
|
42
|
+
to_return(:status => 200, :body => '{}', :headers => {})
|
43
|
+
ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2/')
|
44
|
+
expect(ds.metrics).to be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'raises on request failure' do
|
48
|
+
stub_request(:get, 'http://www.example.com/druid/v2/datasources/test').
|
49
|
+
to_return(:status => 666, :body => 'Strange server error', :headers => {})
|
50
|
+
ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2/')
|
51
|
+
expect { ds.metrics }.to raise_error(RuntimeError)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context '#metadata!' do
|
56
|
+
it 'includes interval in metadata request' do
|
57
|
+
stub = stub_request(:get, 'http://www.example.com/druid/v2/datasources/test?interval=2015-04-10T00:00:00+00:00/2015-04-17T00:00:00+00:00').
|
58
|
+
to_return(:status => 200, :body => '{}', :headers => {})
|
59
|
+
ds = Druid::DataSource.new('test/test', 'http://www.example.com/druid/v2/')
|
60
|
+
ds.metadata!(:interval => ['2015-04-10', '2015-04-17'])
|
61
|
+
expect(stub).to have_been_requested
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/spec/lib/query_spec.rb
CHANGED
@@ -1,142 +1,137 @@
|
|
1
1
|
describe Druid::Query do
|
2
2
|
|
3
3
|
before :each do
|
4
|
-
@query = Druid::Query.new
|
5
|
-
end
|
6
|
-
|
7
|
-
it 'takes a datasource in the constructor' do
|
8
|
-
query = Druid::Query.new('test')
|
9
|
-
JSON.parse(query.to_json)['dataSource'].should == 'test'
|
4
|
+
@query = Druid::Query::Builder.new
|
10
5
|
end
|
11
6
|
|
12
7
|
it 'takes a query type' do
|
13
8
|
@query.query_type('query_type')
|
14
|
-
JSON.parse(@query.to_json)['queryType'].
|
9
|
+
expect(JSON.parse(@query.query.to_json)['queryType']).to eq('query_type')
|
15
10
|
end
|
16
11
|
|
17
12
|
it 'sets query type by group_by' do
|
18
|
-
@query.group_by
|
19
|
-
JSON.parse(@query.to_json)['queryType'].
|
13
|
+
@query.group_by
|
14
|
+
expect(JSON.parse(@query.query.to_json)['queryType']).to eq('groupBy')
|
20
15
|
end
|
21
16
|
|
22
17
|
it 'sets query type to timeseries' do
|
23
|
-
@query.
|
24
|
-
JSON.parse(@query.to_json)['queryType'].
|
18
|
+
@query.timeseries
|
19
|
+
expect(JSON.parse(@query.query.to_json)['queryType']).to eq('timeseries')
|
25
20
|
end
|
26
21
|
|
27
22
|
it 'takes dimensions from group_by method' do
|
28
23
|
@query.group_by(:a, :b, :c)
|
29
|
-
JSON.parse(@query.to_json)['dimensions'].
|
24
|
+
expect(JSON.parse(@query.query.to_json)['dimensions']).to eq(['a', 'b', 'c'])
|
30
25
|
end
|
31
26
|
|
32
27
|
it 'takes dimension, metric and threshold from topn method' do
|
33
28
|
@query.topn(:a, :b, 25)
|
34
|
-
result = JSON.parse(@query.to_json)
|
35
|
-
result['dimension'].
|
36
|
-
result['metric'].
|
37
|
-
result['threshold'].
|
29
|
+
result = JSON.parse(@query.query.to_json)
|
30
|
+
expect(result['dimension']).to eq('a')
|
31
|
+
expect(result['metric']).to eq('b')
|
32
|
+
expect(result['threshold']).to eq(25)
|
38
33
|
end
|
39
34
|
|
40
35
|
describe '#postagg' do
|
41
36
|
it 'build a post aggregation with a constant right' do
|
42
37
|
@query.postagg{(a + 1).as ctr }
|
43
38
|
|
44
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
39
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
45
40
|
"fn"=>"+",
|
46
41
|
"fields"=>
|
47
|
-
[{"type"=>"fieldAccess", "
|
42
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
48
43
|
{"type"=>"constant", "value"=>1}],
|
49
|
-
"name"=>"ctr"}]
|
44
|
+
"name"=>"ctr"}])
|
50
45
|
end
|
51
46
|
|
52
47
|
it 'build a + post aggregation' do
|
53
48
|
@query.postagg{(a + b).as ctr }
|
54
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
49
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
55
50
|
"fn"=>"+",
|
56
51
|
"fields"=>
|
57
|
-
[{"type"=>"fieldAccess",
|
58
|
-
{"type"=>"fieldAccess", "
|
59
|
-
"name"=>"ctr"}]
|
52
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
53
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}],
|
54
|
+
"name"=>"ctr"}])
|
60
55
|
end
|
61
56
|
|
62
57
|
it 'build a - post aggregation' do
|
63
58
|
@query.postagg{(a - b).as ctr }
|
64
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
59
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
65
60
|
"fn"=>"-",
|
66
61
|
"fields"=>
|
67
|
-
[{"type"=>"fieldAccess", "
|
68
|
-
{"type"=>"fieldAccess", "
|
69
|
-
"name"=>"ctr"}]
|
62
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
63
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}],
|
64
|
+
"name"=>"ctr"}])
|
70
65
|
end
|
71
66
|
|
72
67
|
it 'build a * post aggregation' do
|
73
68
|
@query.postagg{(a * b).as ctr }
|
74
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
69
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
75
70
|
"fn"=>"*",
|
76
71
|
"fields"=>
|
77
|
-
[{"type"=>"fieldAccess", "
|
78
|
-
{"type"=>"fieldAccess", "
|
79
|
-
"name"=>"ctr"}]
|
72
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
73
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}],
|
74
|
+
"name"=>"ctr"}])
|
80
75
|
end
|
81
76
|
|
82
77
|
it 'build a / post aggregation' do
|
83
78
|
@query.postagg{(a / b).as ctr }
|
84
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
79
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
85
80
|
"fn"=>"/",
|
86
81
|
"fields"=>
|
87
|
-
[{"type"=>"fieldAccess", "
|
88
|
-
{"type"=>"fieldAccess", "
|
89
|
-
"name"=>"ctr"}]
|
82
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
83
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}],
|
84
|
+
"name"=>"ctr"}])
|
90
85
|
end
|
91
86
|
|
92
87
|
it 'build a complex post aggregation' do
|
93
88
|
@query.postagg{((a / b) * 1000).as ctr }
|
94
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
89
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
95
90
|
"fn"=>"*",
|
96
91
|
"fields"=>
|
97
92
|
[{"type"=>"arithmetic", "fn"=>"/", "fields"=>
|
98
|
-
[{"type"=>"fieldAccess", "
|
99
|
-
{"type"=>"fieldAccess", "
|
93
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
94
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}]},
|
100
95
|
{"type"=>"constant", "value"=>1000}],
|
101
|
-
"name"=>"ctr"}]
|
96
|
+
"name"=>"ctr"}])
|
102
97
|
end
|
103
98
|
|
104
99
|
it 'adds fields required by the postagg operation to longsum' do
|
105
100
|
@query.postagg{ (a/b).as c }
|
106
|
-
JSON.parse(@query.to_json)['aggregations'].
|
101
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq([
|
107
102
|
{"type"=>"longSum", "name"=>"a", "fieldName"=>"a"},
|
108
103
|
{"type"=>"longSum", "name"=>"b", "fieldName"=>"b"}
|
109
|
-
]
|
104
|
+
])
|
110
105
|
end
|
111
106
|
|
112
107
|
it 'chains aggregations' do
|
113
108
|
@query.postagg{(a / b).as ctr }.postagg{(b / a).as rtc }
|
114
109
|
|
115
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
110
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([{"type"=>"arithmetic",
|
116
111
|
"fn"=>"/",
|
117
112
|
"fields"=>
|
118
|
-
[{"type"=>"fieldAccess", "
|
119
|
-
{"type"=>"fieldAccess", "
|
113
|
+
[{"type"=>"fieldAccess", "fieldName"=>"a"},
|
114
|
+
{"type"=>"fieldAccess", "fieldName"=>"b"}],
|
120
115
|
"name"=>"ctr"},
|
121
116
|
{"type"=>"arithmetic",
|
122
117
|
"fn"=>"/",
|
123
118
|
"fields"=>
|
124
|
-
[{"type"=>"fieldAccess", "
|
125
|
-
{"type"=>"fieldAccess", "
|
119
|
+
[{"type"=>"fieldAccess", "fieldName"=>"b"},
|
120
|
+
{"type"=>"fieldAccess", "fieldName"=>"a"}],
|
126
121
|
"name"=>"rtc"}
|
127
|
-
]
|
122
|
+
])
|
128
123
|
end
|
129
124
|
|
130
125
|
it 'builds a javascript post aggregation' do
|
131
126
|
@query.postagg { js('function(agg1, agg2) { return agg1 + agg2; }').as result }
|
132
|
-
JSON.parse(@query.to_json)['postAggregations'].
|
127
|
+
expect(JSON.parse(@query.query.to_json)['postAggregations']).to eq([
|
133
128
|
{
|
134
129
|
'type' => 'javascript',
|
135
130
|
'name' => 'result',
|
136
131
|
'fieldNames' => ['agg1', 'agg2'],
|
137
132
|
'function' => 'function(agg1, agg2) { return agg1 + agg2; }'
|
138
133
|
}
|
139
|
-
]
|
134
|
+
])
|
140
135
|
end
|
141
136
|
|
142
137
|
it 'raises an error when an invalid javascript function is used' do
|
@@ -148,18 +143,75 @@ describe Druid::Query do
|
|
148
143
|
|
149
144
|
it 'builds aggregations on long_sum' do
|
150
145
|
@query.long_sum(:a, :b, :c)
|
151
|
-
JSON.parse(@query.to_json)['aggregations'].
|
146
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq([
|
152
147
|
{ 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
|
153
148
|
{ 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
|
154
149
|
{ 'type' => 'longSum', 'name' => 'c', 'fieldName' => 'c'}
|
155
|
-
]
|
150
|
+
])
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#min' do
|
154
|
+
it 'builds aggregations with "min" type' do
|
155
|
+
@query.min(:a, :b)
|
156
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [
|
157
|
+
{ 'type' => 'min', 'name' => 'a', 'fieldName' => 'a'},
|
158
|
+
{ 'type' => 'min', 'name' => 'b', 'fieldName' => 'b'}
|
159
|
+
]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe '#max' do
|
164
|
+
it 'builds aggregations with "max" type' do
|
165
|
+
@query.max(:a, :b)
|
166
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [
|
167
|
+
{ 'type' => 'max', 'name' => 'a', 'fieldName' => 'a'},
|
168
|
+
{ 'type' => 'max', 'name' => 'b', 'fieldName' => 'b'}
|
169
|
+
]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#hyper_unique' do
|
174
|
+
it 'builds aggregation with "hyperUnique"' do
|
175
|
+
@query.hyper_unique(:a, :b)
|
176
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [
|
177
|
+
{ 'type' => 'hyperUnique', 'name' => 'a', 'fieldName' => 'a'},
|
178
|
+
{ 'type' => 'hyperUnique', 'name' => 'b', 'fieldName' => 'b'}
|
179
|
+
]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe '#cardinality' do
|
184
|
+
it 'builds aggregation with "cardinality" type' do
|
185
|
+
@query.cardinality(:a, [:dim1, :dim2], true)
|
186
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [
|
187
|
+
{ 'type' => 'cardinality', 'name' => 'a', 'fieldNames' => ['dim1', 'dim2'], 'byRow' => true }
|
188
|
+
]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#js_aggregation' do
|
193
|
+
it 'builds aggregation with "javascript" type' do
|
194
|
+
@query.js_aggregation(:aggregate, [:x, :y],
|
195
|
+
aggregate: "function(current, a, b) { return current + (Math.log(a) * b); }",
|
196
|
+
combine: "function(partialA, partialB) { return partialA + partialB; }",
|
197
|
+
reset: "function() { return 10; }"
|
198
|
+
)
|
199
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq [{
|
200
|
+
'type' => 'javascript',
|
201
|
+
'name' => 'aggregate',
|
202
|
+
'fieldNames' => ['x', 'y'],
|
203
|
+
'fnAggregate' => 'function(current, a, b) { return current + (Math.log(a) * b); }',
|
204
|
+
'fnCombine' => 'function(partialA, partialB) { return partialA + partialB; }',
|
205
|
+
'fnReset' => 'function() { return 10; }'
|
206
|
+
}]
|
207
|
+
end
|
156
208
|
end
|
157
209
|
|
158
210
|
it 'appends long_sum properties from aggregations on calling long_sum again' do
|
159
211
|
@query.long_sum(:a, :b, :c)
|
160
212
|
@query.double_sum(:x,:y)
|
161
213
|
@query.long_sum(:d, :e, :f)
|
162
|
-
JSON.parse(@query.to_json)['aggregations'].sort{|x,y| x['name'] <=> y['name']}.
|
214
|
+
expect(JSON.parse(@query.query.to_json)['aggregations'].sort{|x,y| x['name'] <=> y['name']}).to eq([
|
163
215
|
{ 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
|
164
216
|
{ 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
|
165
217
|
{ 'type' => 'longSum', 'name' => 'c', 'fieldName' => 'c'},
|
@@ -168,21 +220,21 @@ describe Druid::Query do
|
|
168
220
|
{ 'type' => 'longSum', 'name' => 'f', 'fieldName' => 'f'},
|
169
221
|
{ 'type' => 'doubleSum', 'name' => 'x', 'fieldName' => 'x'},
|
170
222
|
{ 'type' => 'doubleSum', 'name' => 'y', 'fieldName' => 'y'}
|
171
|
-
]
|
223
|
+
])
|
172
224
|
end
|
173
225
|
|
174
226
|
it 'removes duplicate aggregation fields' do
|
175
227
|
@query.long_sum(:a, :b)
|
176
228
|
@query.long_sum(:b)
|
177
229
|
|
178
|
-
JSON.parse(@query.to_json)['aggregations'].
|
230
|
+
expect(JSON.parse(@query.query.to_json)['aggregations']).to eq([
|
179
231
|
{ 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
|
180
232
|
{ 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
|
181
|
-
]
|
233
|
+
])
|
182
234
|
end
|
183
235
|
|
184
236
|
it 'must be chainable' do
|
185
|
-
q = [Druid::Query.new
|
237
|
+
q = [Druid::Query::Builder.new]
|
186
238
|
q.push q[-1].query_type('a')
|
187
239
|
q.push q[-1].data_source('b')
|
188
240
|
q.push q[-1].group_by('c')
|
@@ -193,274 +245,331 @@ describe Druid::Query do
|
|
193
245
|
q.push q[-1].granularity(:day)
|
194
246
|
|
195
247
|
q.each do |instance|
|
196
|
-
instance.
|
248
|
+
expect(instance).to eq(q[0])
|
197
249
|
end
|
198
250
|
end
|
199
251
|
|
200
252
|
it 'parses intervals from strings' do
|
201
|
-
@query.interval('2013-01-
|
202
|
-
JSON.parse(@query.to_json)['intervals'].
|
253
|
+
@query.interval('2013-01-26T00', '2020-01-26T00:15')
|
254
|
+
expect(JSON.parse(@query.query.to_json)['intervals']).to eq(['2013-01-26T00:00:00+00:00/2020-01-26T00:15:00+00:00'])
|
203
255
|
end
|
204
256
|
|
205
257
|
it 'takes multiple intervals' do
|
206
|
-
@query.intervals([['2013-01-
|
207
|
-
JSON.parse(@query.to_json)['intervals'].
|
258
|
+
@query.intervals([['2013-01-26T00', '2020-01-26T00:15'],['2013-04-23T00', '2013-04-23T15:00']])
|
259
|
+
expect(JSON.parse(@query.query.to_json)['intervals']).to eq(["2013-01-26T00:00:00+00:00/2020-01-26T00:15:00+00:00", "2013-04-23T00:00:00+00:00/2013-04-23T15:00:00+00:00"])
|
208
260
|
end
|
209
261
|
|
210
262
|
it 'accepts Time objects for intervals' do
|
211
263
|
@query.interval(a = Time.now, b = Time.now + 1)
|
212
|
-
JSON.parse(@query.to_json)['intervals'].
|
264
|
+
expect(JSON.parse(@query.query.to_json)['intervals']).to eq(["#{a.iso8601}/#{b.iso8601}"])
|
213
265
|
end
|
214
266
|
|
215
267
|
it 'takes a granularity from string' do
|
216
268
|
@query.granularity('all')
|
217
|
-
JSON.parse(@query.to_json)['granularity'].
|
269
|
+
expect(JSON.parse(@query.query.to_json)['granularity']).to eq('all')
|
218
270
|
end
|
219
271
|
|
220
272
|
it 'should take a period' do
|
221
|
-
@query.granularity(
|
222
|
-
@query.
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
}
|
227
|
-
end
|
273
|
+
@query.granularity("P1D", 'Europe/Berlin')
|
274
|
+
expect(@query.query.as_json['granularity']).to eq({
|
275
|
+
'type' => "period",
|
276
|
+
'period' => "P1D",
|
277
|
+
'timeZone' => "Europe/Berlin"
|
278
|
+
})
|
279
|
+
end
|
280
|
+
|
281
|
+
describe '#filter' do
|
282
|
+
it 'creates a in_circ filter' do
|
283
|
+
@query.filter{a.in_circ [[52.0,13.0], 10.0]}
|
284
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({
|
285
|
+
"type" => "spatial",
|
286
|
+
"dimension" => "a",
|
287
|
+
"bound" => {
|
288
|
+
"type" => "radius",
|
289
|
+
"coords" => [52.0, 13.0],
|
290
|
+
"radius" => 10.0
|
291
|
+
}
|
292
|
+
})
|
293
|
+
end
|
228
294
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
295
|
+
it 'creates a in_rec filter' do
|
296
|
+
@query.filter{a.in_rec [[10.0, 20.0], [30.0, 40.0]] }
|
297
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({
|
298
|
+
"type" => "spatial",
|
299
|
+
"dimension" => "a",
|
300
|
+
"bound" => {
|
301
|
+
"type" => "rectangular",
|
302
|
+
"minCoords" => [10.0, 20.0],
|
303
|
+
"maxCoords" => [30.0, 40.0]
|
304
|
+
}
|
305
|
+
})
|
306
|
+
end
|
241
307
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
"dimension" => "a",
|
247
|
-
"bound" => {
|
248
|
-
"type" => "rectangular",
|
249
|
-
"minCoords" => [10.0, 20.0],
|
250
|
-
"maxCoords" => [30.0, 40.0]
|
251
|
-
}
|
252
|
-
}
|
253
|
-
end
|
308
|
+
it 'creates an equals filter' do
|
309
|
+
@query.filter{a.eq 1}
|
310
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({"type"=>"selector", "dimension"=>"a", "value"=>1})
|
311
|
+
end
|
254
312
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
313
|
+
it 'creates an equals filter with ==' do
|
314
|
+
@query.filter{a == 1}
|
315
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({"type"=>"selector", "dimension"=>"a", "value"=>1})
|
316
|
+
end
|
259
317
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
318
|
+
it 'creates a not filter' do
|
319
|
+
@query.filter{!a.eq 1}
|
320
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"field" =>
|
321
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
322
|
+
"type" => "not"})
|
323
|
+
end
|
264
324
|
|
325
|
+
it 'creates a not filter with neq' do
|
326
|
+
@query.filter{a.neq 1}
|
327
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"field" =>
|
328
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
329
|
+
"type" => "not"})
|
330
|
+
end
|
265
331
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
332
|
+
it 'creates a not filter with !=' do
|
333
|
+
@query.filter{a != 1}
|
334
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"field" =>
|
335
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
336
|
+
"type" => "not"})
|
337
|
+
end
|
272
338
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
339
|
+
it 'creates an and filter' do
|
340
|
+
@query.filter{a.neq(1) & b.eq(2) & c.eq('foo')}
|
341
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
342
|
+
{"type"=>"not", "field"=>{"type"=>"selector", "dimension"=>"a", "value"=>1}},
|
343
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>2},
|
344
|
+
{"type"=>"selector", "dimension"=>"c", "value"=>"foo"}
|
345
|
+
],
|
346
|
+
"type" => "and"})
|
347
|
+
end
|
279
348
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
349
|
+
it 'creates an or filter' do
|
350
|
+
@query.filter{a.neq(1) | b.eq(2) | c.eq('foo')}
|
351
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
352
|
+
{"type"=>"not", "field"=> {"type"=>"selector", "dimension"=>"a", "value"=>1}},
|
353
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>2},
|
354
|
+
{"type"=>"selector", "dimension"=>"c", "value"=>"foo"}
|
355
|
+
],
|
356
|
+
"type" => "or"})
|
357
|
+
end
|
286
358
|
|
359
|
+
it 'chains filters' do
|
360
|
+
@query.filter{a.eq(1)}.filter{b.eq(2)}
|
361
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
362
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
363
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>2}
|
364
|
+
],
|
365
|
+
"type" => "and"})
|
366
|
+
end
|
287
367
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
end
|
368
|
+
it 'creates filter from hash' do
|
369
|
+
@query.filter a:1, b:2
|
370
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
371
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
372
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>2}
|
373
|
+
],
|
374
|
+
"type" => "and"})
|
375
|
+
end
|
297
376
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
377
|
+
context 'when type argument is :nin' do
|
378
|
+
it 'creates nin filter from hash' do
|
379
|
+
@query.filter({ a: 1, b: 2 }, :nin)
|
380
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({'fields' => [
|
381
|
+
{'type' => 'not', 'field' => { 'dimension' => 'a', 'type' => 'selector', 'value' => 1} },
|
382
|
+
{'type' => 'not', 'field' => { 'dimension' => 'b', 'type' => 'selector', 'value' => 2} }
|
383
|
+
],
|
384
|
+
'type' => 'and'})
|
385
|
+
end
|
386
|
+
end
|
307
387
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
388
|
+
it 'creates an in statement with or filter' do
|
389
|
+
@query.filter{a.in [1,2,3]}
|
390
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
391
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
392
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>2},
|
393
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>3}
|
394
|
+
],
|
395
|
+
"type" => "or"})
|
396
|
+
end
|
316
397
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
398
|
+
it 'creates a nin statement with and filter' do
|
399
|
+
@query.filter{a.nin [1,2,3]}
|
400
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq( {"fields" => [
|
401
|
+
{"field"=>{"type"=>"selector", "dimension"=>"a", "value"=>1},"type" => "not"},
|
402
|
+
{"field"=>{"type"=>"selector", "dimension"=>"a", "value"=>2},"type" => "not"},
|
403
|
+
{"field"=>{"type"=>"selector", "dimension"=>"a", "value"=>3},"type" => "not"}
|
404
|
+
],
|
405
|
+
"type" => "and"})
|
406
|
+
end
|
324
407
|
|
325
|
-
|
408
|
+
it 'creates a javascript with > filter' do
|
409
|
+
@query.filter{a > 100}
|
410
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({
|
411
|
+
"type" => "javascript",
|
412
|
+
"dimension" => "a",
|
413
|
+
"function" => "function(a) { return(a > 100); }"
|
414
|
+
})
|
415
|
+
end
|
326
416
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
end
|
417
|
+
it 'creates a mixed javascript filter' do
|
418
|
+
@query.filter{(a >= 128) & (a != 256)}
|
419
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({"fields" => [
|
420
|
+
{"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a >= 128); }"},
|
421
|
+
{"field" => {"type" => "selector", "dimension" => "a", "value" => 256}, "type" => "not"}
|
422
|
+
],
|
423
|
+
"type" => "and"})
|
424
|
+
end
|
336
425
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
end
|
426
|
+
it 'creates a complex javascript filter' do
|
427
|
+
@query.filter{(a >= 4) & (a <= '128')}
|
428
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({"fields" => [
|
429
|
+
{"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a >= 4); }"},
|
430
|
+
{"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a <= \"128\"); }"}
|
431
|
+
],
|
432
|
+
"type" => "and"})
|
433
|
+
end
|
346
434
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
435
|
+
it 'creates a custom javascript filter' do
|
436
|
+
@query.filter{a.javascript("function(a) { return true; }")}
|
437
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({
|
438
|
+
"type" => "javascript",
|
439
|
+
"dimension" => "a",
|
440
|
+
"function" => "function(a) { return true; }"
|
441
|
+
})
|
442
|
+
end
|
355
443
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
444
|
+
it 'can chain two in statements' do
|
445
|
+
@query.filter{a.in([1,2,3]) & b.in([1,2,3])}
|
446
|
+
expect(JSON.parse(@query.query.to_json)['filter']).to eq({"type"=>"and", "fields"=>[
|
447
|
+
{"type"=>"or", "fields"=>[
|
448
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
449
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>2},
|
450
|
+
{"type"=>"selector", "dimension"=>"a", "value"=>3}
|
451
|
+
]},
|
452
|
+
{"type"=>"or", "fields"=>[
|
453
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>1},
|
454
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>2},
|
455
|
+
{"type"=>"selector", "dimension"=>"b", "value"=>3}
|
456
|
+
]}
|
457
|
+
]})
|
458
|
+
end
|
363
459
|
end
|
364
460
|
|
365
|
-
|
366
|
-
|
367
|
-
JSON.parse(@query.to_json)['filter'].should == {"fields" => [
|
368
|
-
{"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a >= 4); }"},
|
369
|
-
{"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a <= '128'); }"}
|
370
|
-
],
|
371
|
-
"type" => "and"}
|
372
|
-
end
|
461
|
+
describe '#having' do
|
462
|
+
subject(:having) { JSON.parse(@query.to_json)['having'] }
|
373
463
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
{"type"=>"selector", "dimension"=>"a", "value"=>1},
|
379
|
-
{"type"=>"selector", "dimension"=>"a", "value"=>2},
|
380
|
-
{"type"=>"selector", "dimension"=>"a", "value"=>3}
|
381
|
-
]},
|
382
|
-
{"type"=>"or", "fields"=>[
|
383
|
-
{"type"=>"selector", "dimension"=>"b", "value"=>1},
|
384
|
-
{"type"=>"selector", "dimension"=>"b", "value"=>2},
|
385
|
-
{"type"=>"selector", "dimension"=>"b", "value"=>3}
|
386
|
-
]}
|
387
|
-
]}
|
388
|
-
end
|
464
|
+
it 'creates an equalTo clause using ==' do
|
465
|
+
@query.having { a == 100 }
|
466
|
+
expect(@query.query.as_json['having']).to eq({ 'type' => 'equalTo', 'aggregation' => 'a', 'value' => 100 })
|
467
|
+
end
|
389
468
|
|
390
|
-
|
391
|
-
|
392
|
-
@query.having{
|
393
|
-
|
394
|
-
|
395
|
-
}
|
396
|
-
end
|
397
|
-
|
398
|
-
it 'chains having clauses with and' do
|
399
|
-
@query.having{a > 100}.having{b > 200}.having{c > 300}
|
400
|
-
JSON.parse(@query.to_json)['having'].should == {
|
401
|
-
"type" => "and",
|
402
|
-
"havingSpecs" => [
|
403
|
-
{ "type" => "greaterThan", "aggregation" => "a", "value" => 100 },
|
404
|
-
{ "type" => "greaterThan", "aggregation" => "b", "value" => 200 },
|
405
|
-
{ "type" => "greaterThan", "aggregation" => "c", "value" => 300 }
|
406
|
-
]
|
407
|
-
}
|
469
|
+
it 'creates a not equalTo clause using !=' do
|
470
|
+
@query.having { a != 100 }
|
471
|
+
expect(@query.query.as_json['having']).to eq({
|
472
|
+
'type' => 'not',
|
473
|
+
'havingSpec' => { 'type' => 'equalTo', 'aggregation' => 'a', 'value' => 100 },
|
474
|
+
})
|
408
475
|
end
|
409
|
-
end
|
410
476
|
|
411
|
-
|
412
|
-
|
413
|
-
|
477
|
+
it 'creates a greaterThan clause using >' do
|
478
|
+
@query.having { a > 100 }
|
479
|
+
expect(@query.query.as_json['having']).to eq({ 'type' => 'greaterThan', 'aggregation' => 'a', 'value' => 100 })
|
480
|
+
end
|
414
481
|
|
415
|
-
|
416
|
-
|
417
|
-
|
482
|
+
it 'creates a lessThan clause using <' do
|
483
|
+
@query.having { a < 100 }
|
484
|
+
expect(@query.query.as_json['having']).to eq({ 'type' => 'lessThan', 'aggregation' => 'a', 'value' => 100 })
|
485
|
+
end
|
418
486
|
|
419
|
-
|
420
|
-
|
421
|
-
|
487
|
+
it 'creates an add clause using &' do
|
488
|
+
@query.having { (a > 100) & (b > 200) }
|
489
|
+
expect(@query.query.as_json['having']).to eq({
|
490
|
+
'type' => 'and',
|
491
|
+
'havingSpecs' => [
|
492
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'a', 'value' => 100 },
|
493
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'b', 'value' => 200 },
|
494
|
+
]
|
495
|
+
})
|
496
|
+
end
|
497
|
+
|
498
|
+
it 'creates an or clause using |' do
|
499
|
+
@query.having { (a > 100) | (b > 200) }
|
500
|
+
expect(@query.query.as_json['having']).to eq({
|
501
|
+
'type' => 'or',
|
502
|
+
'havingSpecs' => [
|
503
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'a', 'value' => 100 },
|
504
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'b', 'value' => 200 },
|
505
|
+
]
|
506
|
+
})
|
507
|
+
end
|
508
|
+
|
509
|
+
it 'creates a not clause using !' do
|
510
|
+
@query.having { !((a == 100) & (b == 200)) }
|
511
|
+
expect(@query.query.as_json['having']).to eq({
|
512
|
+
'type' => 'not',
|
513
|
+
'havingSpec' => {
|
514
|
+
'type' => 'and',
|
515
|
+
'havingSpecs' => [
|
516
|
+
{ 'type' => 'equalTo', 'aggregation' => 'a', 'value' => 100 },
|
517
|
+
{ 'type' => 'equalTo', 'aggregation' => 'b', 'value' => 200 },
|
518
|
+
]
|
519
|
+
}
|
520
|
+
})
|
521
|
+
end
|
422
522
|
|
423
|
-
|
424
|
-
|
523
|
+
it 'combines successive calls with and operator' do
|
524
|
+
@query.having { a > 100 }.having { b > 200 }.having { c > 300 }
|
525
|
+
expect(@query.query.as_json['having']).to eq({
|
526
|
+
'type' => 'and',
|
527
|
+
'havingSpecs' => [
|
528
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'a', 'value' => 100 },
|
529
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'b', 'value' => 200 },
|
530
|
+
{ 'type' => 'greaterThan', 'aggregation' => 'c', 'value' => 300 },
|
531
|
+
]
|
532
|
+
})
|
533
|
+
end
|
425
534
|
end
|
426
535
|
|
427
536
|
it 'should query regexp using .regexp(string)' do
|
428
|
-
JSON.parse(@query.filter{a.regexp('[1-9].*')}.to_json)['filter'].
|
537
|
+
expect(JSON.parse(@query.filter{a.regexp('[1-9].*')}.query.to_json)['filter']).to eq({
|
429
538
|
"dimension"=>"a",
|
430
539
|
"type"=>"regex",
|
431
540
|
"pattern"=>"[1-9].*"
|
432
|
-
}
|
541
|
+
})
|
433
542
|
end
|
434
543
|
|
435
544
|
it 'should query regexp using .eq(regexp)' do
|
436
|
-
JSON.parse(@query.filter{a.in(/abc.*/)}.to_json)['filter'].
|
545
|
+
expect(JSON.parse(@query.filter{a.in(/abc.*/)}.query.to_json)['filter']).to eq({
|
437
546
|
"dimension"=>"a",
|
438
547
|
"type"=>"regex",
|
439
548
|
"pattern"=>"abc.*"
|
440
|
-
}
|
549
|
+
})
|
441
550
|
end
|
442
551
|
|
443
552
|
it 'should query regexp using .in([regexp])' do
|
444
|
-
JSON.parse(@query.filter{ a.in(['b', /[a-z].*/, 'c']) }.to_json)['filter'].
|
553
|
+
expect(JSON.parse(@query.filter{ a.in(['b', /[a-z].*/, 'c']) }.query.to_json)['filter']).to eq({
|
445
554
|
"type"=>"or",
|
446
555
|
"fields"=>[
|
447
556
|
{"dimension"=>"a", "type"=>"selector", "value"=>"b"},
|
448
557
|
{"dimension"=>"a", "type"=>"regex", "pattern"=>"[a-z].*"},
|
449
558
|
{"dimension"=>"a", "type"=>"selector", "value"=>"c"}
|
450
559
|
]
|
451
|
-
}
|
560
|
+
})
|
452
561
|
end
|
453
562
|
|
454
563
|
it 'takes type, limit and columns from limit method' do
|
455
|
-
@query.
|
456
|
-
result = JSON.parse(@query.to_json)
|
457
|
-
result['limitSpec'].
|
564
|
+
@query.limit(10, :a => 'ASCENDING', :b => 'DESCENDING')
|
565
|
+
result = JSON.parse(@query.query.to_json)
|
566
|
+
expect(result['limitSpec']).to eq({
|
458
567
|
'type' => 'default',
|
459
568
|
'limit' => 10,
|
460
569
|
'columns' => [
|
461
570
|
{ 'dimension' => 'a', 'direction' => 'ASCENDING'},
|
462
571
|
{ 'dimension' => 'b', 'direction' => 'DESCENDING'}
|
463
572
|
]
|
464
|
-
}
|
573
|
+
})
|
465
574
|
end
|
466
575
|
end
|