clickhouse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require_relative "../../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ module Query
6
+ class TestTable < MiniTest::Test
7
+
8
+ describe Clickhouse::Connection::Query::Table do
9
+ it "generates a 'CREATE TABLE' statement" do
10
+ table = Clickhouse::Connection::Query::Table.new("logs_test") do |t|
11
+ t.uint8 :id
12
+ t.float32 :price
13
+ t.string :name
14
+ t.date :date
15
+ t.date_time :time
16
+ t.fixed_string :hex_id, 8
17
+ t.engine "MergeTree(date, 8192)"
18
+ end
19
+
20
+ sql = <<-SQL
21
+ CREATE TABLE logs_test (
22
+ id UInt8,
23
+ price Float32,
24
+ name String,
25
+ date Date,
26
+ time DateTime,
27
+ hex_id FixedString(8)
28
+ )
29
+ ENGINE = MergeTree(date, 8192)
30
+ SQL
31
+
32
+ assert_equal sql.strip, table.to_sql.strip
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,131 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ class TestClient < MiniTest::Test
6
+
7
+ class Connection < SimpleConnection
8
+ include Clickhouse::Connection::Client
9
+ end
10
+
11
+ describe Clickhouse::Connection::Client do
12
+ before do
13
+ @connection = Connection.new
14
+ end
15
+
16
+ describe "#connect!" do
17
+ describe "when failed to connect" do
18
+ it "returns true" do
19
+ Faraday::Connection.any_instance.expects(:get).raises(Faraday::ConnectionFailed.new("Failed to connect"))
20
+ assert_raises Clickhouse::ConnectionError do
21
+ @connection.connect!
22
+ end
23
+ end
24
+ end
25
+
26
+ describe "when receiving 200" do
27
+ it "returns true" do
28
+ Faraday::Connection.any_instance.expects(:get).returns(stub(:status => 200))
29
+ assert_equal true, @connection.connect!
30
+ end
31
+ end
32
+
33
+ describe "when receiving 500" do
34
+ it "raises a Clickhouse::ConnectionError" do
35
+ Faraday::Connection.any_instance.expects(:get).returns(stub(:status => 500))
36
+ assert_raises Clickhouse::ConnectionError do
37
+ @connection.connect!
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "when already connected" do
43
+ it "returns nil" do
44
+ @connection.instance_variable_set :@client, mock
45
+ assert_nil @connection.connect!
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#connected?" do
51
+ it "returns whether it has an connected socket" do
52
+ assert_equal false, @connection.connected?
53
+ @connection.instance_variable_set :@client, mock
54
+ assert_equal true, @connection.connected?
55
+ @connection.instance_variable_set :@client, nil
56
+ assert_equal false, @connection.connected?
57
+ end
58
+ end
59
+
60
+ describe "#get" do
61
+ it "sends a GET request the server" do
62
+ @connection.instance_variable_set :@client, (client = mock)
63
+ client.expects(:get).with("/?query=foo", nil).returns(stub(:status => 200, :body => ""))
64
+ @connection.stubs(:log)
65
+ @connection.get(:foo)
66
+ end
67
+ end
68
+
69
+ describe "#post" do
70
+ it "sends a POST request the server" do
71
+ @connection.instance_variable_set :@client, (client = mock)
72
+ client.expects(:post).with("/?query=foo", "body").returns(stub(:status => 200, :body => ""))
73
+ @connection.stubs(:log)
74
+ @connection.post(:foo, "body")
75
+ end
76
+ end
77
+
78
+ describe "#request" do
79
+ before do
80
+ @connection.expects(:log)
81
+ end
82
+
83
+ it "connects to the server first" do
84
+ @connection.instance_variable_set :@client, (client = mock)
85
+ @connection.expects(:connect!)
86
+ client.stubs(:get).returns(stub(:status => 200, :body => ""))
87
+ @connection.send :request, :get, "/", "query"
88
+ end
89
+
90
+ it "queries the server returning the response" do
91
+ @connection.instance_variable_set :@client, (client = mock)
92
+ client.expects(:get).with("/?query=SELECT+1", nil).returns(response = stub(:status => 200, :body => ""))
93
+ assert_equal response, @connection.send(:request, :get, "SELECT 1")
94
+ end
95
+
96
+ describe "when not receiving status 200" do
97
+ it "raises a Clickhouse::QueryError" do
98
+ @connection.instance_variable_set :@client, (client = mock)
99
+ client.expects(:get).with("/?query=SELECT+1", nil).returns(stub(:status => 500, :body => ""))
100
+ assert_raises Clickhouse::QueryError do
101
+ @connection.send(:request, :get, "SELECT 1")
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "configuration" do
108
+ describe "database" do
109
+ it "includes the database in the querystring" do
110
+ @connection.expects(:log)
111
+ @connection.instance_variable_get(:@config)[:database] = "system"
112
+ @connection.instance_variable_set(:@client, (client = mock))
113
+ client.expects(:get).with("/?database=system&query=SELECT+1", nil).returns(response = stub(:status => 200, :body => ""))
114
+ assert_equal response, @connection.send(:request, :get, "SELECT 1")
115
+ end
116
+ end
117
+
118
+ describe "authentication" do
119
+ it "includes the credentials in the request headers" do
120
+ connection = Clickhouse::Connection.new :password => "awesomepassword"
121
+ connection.expects(:ping!)
122
+ connection.connect!
123
+ assert_equal "Basic ZGVmYXVsdDphd2Vzb21lcGFzc3dvcmQ=", connection.send(:client).headers["Authorization"].force_encoding("UTF-8")
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ class TestLogger < MiniTest::Test
6
+
7
+ class Connection < SimpleConnection
8
+ include Clickhouse::Connection::Logger
9
+ end
10
+
11
+ describe Clickhouse::Connection::Logger do
12
+ before do
13
+ @connection = Connection.new
14
+ end
15
+
16
+ describe "#log" do
17
+ describe "when having specified a logger" do
18
+ it "delegates to logger" do
19
+ (logger = mock).expects(:info, "Hello world!")
20
+ Clickhouse.expects(:logger).returns(logger).twice
21
+ @connection.send(:log, :info, "Hello world!")
22
+ end
23
+ end
24
+
25
+ describe "when not having specified a logger" do
26
+ it "does nothing" do
27
+ assert_nil @connection.send(:log, :info, "Boo!")
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,391 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ class TestQuery < MiniTest::Test
6
+
7
+ class Connection < SimpleConnection
8
+ include Clickhouse::Connection::Query
9
+ include Clickhouse::Connection::Logger
10
+ end
11
+
12
+ describe Clickhouse::Connection::Query do
13
+ before do
14
+ @connection = Connection.new
15
+ end
16
+
17
+ describe "#execute" do
18
+ it "sends a POST request" do
19
+ @connection.expects(:post).with("sql", nil).returns(stub(:status => 200, :body => ""))
20
+ assert_equal true, @connection.execute("sql")
21
+ end
22
+
23
+ describe "when server returns a non-empty body" do
24
+ it "returns the body of the response" do
25
+ @connection.expects(:post).with("sql", "body").returns(stub(:status => 200, :body => "Ok."))
26
+ assert_equal "Ok.", @connection.execute("sql", "body")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#query" do
32
+ it "sends a GET request requesting a TSV response including names and types" do
33
+ @connection.expects(:get).with("sql FORMAT TabSeparatedWithNamesAndTypes").returns(stub(:status => 200, :body => ""))
34
+ assert_equal [], @connection.query("sql").to_a
35
+ end
36
+ end
37
+
38
+ describe "#databases" do
39
+ it "sends a 'SHOW DATABASES' query" do
40
+ @connection.expects(:get).with("SHOW DATABASES FORMAT TabSeparatedWithNamesAndTypes").returns(stub(:status => 200, :body => ""))
41
+ @connection.databases
42
+ end
43
+ end
44
+
45
+ describe "#tables" do
46
+ it "sends a 'SHOW TABLES' query" do
47
+ @connection.expects(:get).with("SHOW TABLES FORMAT TabSeparatedWithNamesAndTypes").returns(stub(:status => 200, :body => ""))
48
+ @connection.tables
49
+ end
50
+ end
51
+
52
+ describe "#create_table" do
53
+ it "sends a 'CREATE TABLE' query" do
54
+ sql = <<-SQL
55
+ CREATE TABLE logs_test (
56
+ id UInt8,
57
+ price Float32,
58
+ name String,
59
+ date Date,
60
+ time DateTime,
61
+ hex_id FixedString(8)
62
+ )
63
+ ENGINE = MergeTree(date, 8192)
64
+ SQL
65
+ @connection.expects(:post).with(sql.strip, nil).returns(stub(:status => 200, :body => ""))
66
+ @connection.create_table("logs_test") do |t|
67
+ t.uint8 :id
68
+ t.float32 :price
69
+ t.string :name
70
+ t.date :date
71
+ t.date_time :time
72
+ t.fixed_string :hex_id, 8
73
+ t.engine "MergeTree(date, 8192)"
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "#describe_table" do
79
+ it "sends a 'DESCRIBE TABLE <name>' query" do
80
+ @connection.expects(:get).with("DESCRIBE TABLE logs FORMAT TabSeparatedWithNamesAndTypes").returns(stub(:status => 200, :body => ""))
81
+ @connection.describe_table("logs")
82
+ end
83
+ end
84
+
85
+ describe "#rename_table" do
86
+ describe "when passing an array with an even number of names" do
87
+ it "sends a POST request containing a RENAME TABLE statement" do
88
+ @connection.expects(:post).with("RENAME TABLE foo TO bar, baz TO qux", nil).returns(stub(:status => 200, :body => "")).twice
89
+ assert_equal true, @connection.rename_table("foo", "bar", "baz", "qux")
90
+ assert_equal true, @connection.rename_table(["foo", "bar"], ["baz", "qux"])
91
+ end
92
+ end
93
+
94
+ describe "when passing an array with an odd number of names" do
95
+ it "raises an Clickhouse::InvalidQueryError" do
96
+ assert_raises Clickhouse::InvalidQueryError do
97
+ @connection.rename_table "foo"
98
+ end
99
+ assert_raises Clickhouse::InvalidQueryError do
100
+ @connection.rename_table ["foo"]
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "when passing a hash" do
106
+ it "sends a POST request containing a RENAME TABLE statement" do
107
+ @connection.expects(:post).with("RENAME TABLE foo TO bar, baz TO qux", nil).returns(stub(:status => 200, :body => ""))
108
+ assert_equal true, @connection.rename_table(:foo => "bar", :baz => "qux")
109
+ end
110
+ end
111
+ end
112
+
113
+ describe "#drop_table" do
114
+ it "sends a POST request containing a 'DROP TABLE' statement" do
115
+ @connection.expects(:post).with("DROP TABLE logs", nil).returns(stub(:status => 200, :body => ""))
116
+ assert_equal true, @connection.drop_table("logs")
117
+ end
118
+ end
119
+
120
+ describe "#insert_rows" do
121
+ before do
122
+ @csv = <<-CSV
123
+ id,first_name,last_name
124
+ 12345,Paul,Engel
125
+ 67890,Bruce,Wayne
126
+ CSV
127
+ @csv.gsub!(/^\s+/, "")
128
+ end
129
+
130
+ describe "when using hashes" do
131
+ it "sends a POST request containing a 'INSERT INTO' statement using CSV" do
132
+ @connection.expects(:post).with("INSERT INTO logs FORMAT CSVWithNames", @csv).returns(stub(:status => 200, :body => ""))
133
+ assert_equal true, @connection.insert_rows("logs") { |rows|
134
+ rows << {:id => 12345, :first_name => "Paul", :last_name => "Engel"}
135
+ rows << {:id => 67890, :first_name => "Bruce", :last_name => "Wayne"}
136
+ }
137
+ end
138
+ end
139
+
140
+ describe "when using arrays" do
141
+ it "sends a POST request containing a 'INSERT INTO' statement using CSV" do
142
+ @connection.expects(:post).with("INSERT INTO logs FORMAT CSVWithNames", @csv).returns(stub(:status => 200, :body => ""))
143
+ assert_equal true, @connection.insert_rows("logs", :names => %w(id first_name last_name)) { |rows|
144
+ rows << [12345, "Paul", "Engel"]
145
+ rows << [67890, "Bruce", "Wayne"]
146
+ }
147
+ end
148
+ end
149
+ end
150
+
151
+ describe "#select_rows" do
152
+ it "sends a GET request and parses the result set" do
153
+ body = <<-TSV
154
+ year\tname
155
+ UInt16\tString
156
+ 1982\tPaul
157
+ 1947\tAnna
158
+ TSV
159
+
160
+ @connection.expects(:to_select_query).with(options = {:from => "logs"})
161
+ @connection.expects(:get).returns(stub(:body => body.gsub(/^\s+/, "")))
162
+
163
+ assert_equal [
164
+ [1982, "Paul"],
165
+ [1947, "Anna"]
166
+ ], @connection.select_rows(options).to_a
167
+ end
168
+ end
169
+
170
+ describe "#select_row" do
171
+ it "returns an empty array" do
172
+ @connection.expects(:select_rows).returns([["Paul", "Engel"], ["Bruce", "Wayne"]])
173
+ assert_equal ["Paul", "Engel"], @connection.select_row({})
174
+ end
175
+ end
176
+
177
+ describe "#select_values" do
178
+ describe "when empty result set" do
179
+ it "returns an empty array" do
180
+ @connection.expects(:to_select_query)
181
+ @connection.expects(:get).returns(stub(:body => ""))
182
+ assert_equal [], @connection.select_values({})
183
+ end
184
+ end
185
+
186
+ describe "when getting data" do
187
+ it "returns every first value of every row" do
188
+ body = <<-TSV
189
+ year\tname
190
+ UInt16\tString
191
+ 1982\tPaul
192
+ 1947\tAnna
193
+ TSV
194
+
195
+ @connection.expects(:to_select_query)
196
+ @connection.expects(:get).returns(stub(:body => body.gsub(/^\s+/, "")))
197
+ assert_equal [
198
+ 1982,
199
+ 1947
200
+ ], @connection.select_values({})
201
+ end
202
+ end
203
+ end
204
+
205
+ describe "#select_value" do
206
+ describe "when empty result set" do
207
+ it "returns nil" do
208
+ @connection.expects(:select_values).with(options = {:foo => "bar"}).returns([])
209
+ assert_nil @connection.select_value(options)
210
+ end
211
+ end
212
+
213
+ describe "when getting data" do
214
+ it "returns the first value of the first row" do
215
+ @connection.expects(:select_values).with(options = {:foo => "bar"}).returns([1982])
216
+ assert_equal 1982, @connection.select_value(options)
217
+ end
218
+ end
219
+ end
220
+
221
+ describe "#count" do
222
+ it "returns the first value of the first row" do
223
+ @connection.expects(:select_value).with(:select => "COUNT(*)", :from => "logs").returns(1982)
224
+ assert_equal 1982, @connection.count(:from => "logs")
225
+ end
226
+ end
227
+
228
+ describe "#to_select_query" do
229
+ describe "when passing :from option" do
230
+ it "generates a simple 'SELECT * FROM <table>' query" do
231
+ query = <<-SQL
232
+ SELECT *
233
+ FROM logs
234
+ SQL
235
+ options = {
236
+ :from => "logs"
237
+ }
238
+ assert_query(query, @connection.send(:to_select_query, options))
239
+ end
240
+ end
241
+
242
+ describe "when passing :from and :select option" do
243
+ describe "when passing a single column" do
244
+ it "respects the single column in the SELECT statement" do
245
+ query = <<-SQL
246
+ SELECT MIN(date)
247
+ FROM logs
248
+ SQL
249
+ options = {
250
+ :select => "MIN(date)",
251
+ :from => "logs"
252
+ }
253
+ assert_query(query, @connection.send(:to_select_query, options))
254
+ end
255
+ end
256
+
257
+ describe "when passing multiple columns" do
258
+ it "only includes the passed columns in the SELECT statement" do
259
+ query = <<-SQL
260
+ SELECT MIN(date), MAX(date)
261
+ FROM logs
262
+ SQL
263
+ options = {
264
+ :select => ["MIN(date)", "MAX(date)"],
265
+ :from => "logs"
266
+ }
267
+ assert_query(query, @connection.send(:to_select_query, options))
268
+ end
269
+ end
270
+
271
+ describe "when filtering on value is empty" do
272
+ it "uses the empty() function in the WHERE statement" do
273
+ query = <<-SQL
274
+ SELECT *
275
+ FROM logs
276
+ WHERE empty(parent_id)
277
+ SQL
278
+ options = {
279
+ :from => "logs",
280
+ :where => {
281
+ :parent_id => :empty
282
+ }
283
+ }
284
+ assert_query(query, @connection.send(:to_select_query, options))
285
+ end
286
+ end
287
+
288
+ describe "when filtering on value is within a certain range" do
289
+ it "includes the range in the WHERE statement" do
290
+ query = <<-SQL
291
+ SELECT *
292
+ FROM logs
293
+ WHERE code >= 6 AND code <= 10
294
+ SQL
295
+ options = {
296
+ :from => "logs",
297
+ :where => {
298
+ :code => 6..10
299
+ }
300
+ }
301
+ assert_query(query, @connection.send(:to_select_query, options))
302
+ end
303
+ end
304
+
305
+ describe "when filtering on value in array" do
306
+ it "uses an IN operator in the WHERE statement" do
307
+ query = <<-SQL
308
+ SELECT *
309
+ FROM logs
310
+ WHERE code IN (6, 7, 8, 9, 10)
311
+ SQL
312
+ options = {
313
+ :from => "logs",
314
+ :where => {
315
+ :code => [6, 7, 8, 9, 10]
316
+ }
317
+ }
318
+ assert_query(query, @connection.send(:to_select_query, options))
319
+ end
320
+ end
321
+
322
+ describe "when filtering using backticks" do
323
+ it "uses the specified SQL as is" do
324
+ query = <<-SQL
325
+ SELECT *
326
+ FROM logs
327
+ WHERE id != 'cb5a67d2932911e6'
328
+ SQL
329
+ options = {
330
+ :from => "logs",
331
+ :where => {
332
+ :id => "`!= 'cb5a67d2932911e6'`"
333
+ }
334
+ }
335
+ assert_query(query, @connection.send(:to_select_query, options))
336
+ end
337
+ end
338
+
339
+ describe "when filtering on a string" do
340
+ it "uses a single quoted string" do
341
+ query = <<-SQL
342
+ SELECT *
343
+ FROM logs
344
+ WHERE id = 'cb5a67d2932911e6'
345
+ SQL
346
+ options = {
347
+ :from => "logs",
348
+ :where => {
349
+ :id => "cb5a67d2932911e6"
350
+ }
351
+ }
352
+ assert_query(query, @connection.send(:to_select_query, options))
353
+ end
354
+ end
355
+
356
+ describe "when using all options" do
357
+ it "generates the complex query" do
358
+ query = <<-SQL
359
+ SELECT date, COUNT(id), groupUniqArray(severity), SUM(clicks)
360
+ FROM logs
361
+ WHERE date >= '2016-08-01' AND hidden = 0
362
+ GROUP BY date
363
+ HAVING MIN(severity) = 2
364
+ ORDER BY MIN(time) DESC
365
+ LIMIT 120, 60
366
+ SQL
367
+ options = {
368
+ :select => ["date", "COUNT(id)", "groupUniqArray(severity)", "SUM(clicks)"],
369
+ :from => "logs",
370
+ :where => {
371
+ :date => "`>= '2016-08-01'`",
372
+ :hidden => 0
373
+ },
374
+ :group => "date",
375
+ :having => {
376
+ "MIN(severity)" => 2
377
+ },
378
+ :order => "MIN(time) DESC",
379
+ :limit => 60,
380
+ :offset => 120
381
+ }
382
+ assert_query(query, @connection.send(:to_select_query, options))
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+
389
+ end
390
+ end
391
+ end