ruby-druid 0.1.9 → 0.9.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.
- 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
|