ch-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +22 -0
  3. data/.gitignore +9 -0
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG.md +58 -0
  6. data/Gemfile +3 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +262 -0
  9. data/Rakefile +15 -0
  10. data/VERSION +1 -0
  11. data/bin/clickhouse +9 -0
  12. data/clickhouse.gemspec +36 -0
  13. data/lib/clickhouse.rb +60 -0
  14. data/lib/clickhouse/cli.rb +46 -0
  15. data/lib/clickhouse/cli/client.rb +149 -0
  16. data/lib/clickhouse/cli/console.rb +73 -0
  17. data/lib/clickhouse/cli/server.rb +37 -0
  18. data/lib/clickhouse/cli/server/assets/css/clickhouse.css +177 -0
  19. data/lib/clickhouse/cli/server/assets/css/codemirror.css +341 -0
  20. data/lib/clickhouse/cli/server/assets/css/datatables.css +1 -0
  21. data/lib/clickhouse/cli/server/assets/css/normalize.css +427 -0
  22. data/lib/clickhouse/cli/server/assets/css/skeleton.css +418 -0
  23. data/lib/clickhouse/cli/server/assets/js/clickhouse.js +188 -0
  24. data/lib/clickhouse/cli/server/assets/js/codemirror.js +9096 -0
  25. data/lib/clickhouse/cli/server/assets/js/datatables.js +166 -0
  26. data/lib/clickhouse/cli/server/assets/js/disableswipeback.js +97 -0
  27. data/lib/clickhouse/cli/server/assets/js/jquery.js +11015 -0
  28. data/lib/clickhouse/cli/server/assets/js/sql.js +232 -0
  29. data/lib/clickhouse/cli/server/views/index.erb +46 -0
  30. data/lib/clickhouse/cluster.rb +43 -0
  31. data/lib/clickhouse/connection.rb +42 -0
  32. data/lib/clickhouse/connection/client.rb +135 -0
  33. data/lib/clickhouse/connection/logger.rb +12 -0
  34. data/lib/clickhouse/connection/query.rb +160 -0
  35. data/lib/clickhouse/connection/query/result_row.rb +36 -0
  36. data/lib/clickhouse/connection/query/result_set.rb +103 -0
  37. data/lib/clickhouse/connection/query/table.rb +50 -0
  38. data/lib/clickhouse/error.rb +18 -0
  39. data/lib/clickhouse/utils.rb +23 -0
  40. data/lib/clickhouse/version.rb +7 -0
  41. data/script/console +58 -0
  42. data/test/test_helper.rb +15 -0
  43. data/test/test_helper/coverage.rb +16 -0
  44. data/test/test_helper/minitest.rb +13 -0
  45. data/test/test_helper/simple_connection.rb +12 -0
  46. data/test/unit/connection/query/test_result_row.rb +36 -0
  47. data/test/unit/connection/query/test_result_set.rb +196 -0
  48. data/test/unit/connection/query/test_table.rb +39 -0
  49. data/test/unit/connection/test_client.rb +206 -0
  50. data/test/unit/connection/test_cluster.rb +81 -0
  51. data/test/unit/connection/test_logger.rb +35 -0
  52. data/test/unit/connection/test_query.rb +410 -0
  53. data/test/unit/test_clickhouse.rb +99 -0
  54. data/test/unit/test_connection.rb +55 -0
  55. data/test/unit/test_utils.rb +39 -0
  56. metadata +326 -0
@@ -0,0 +1,81 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ class TestCluser < MiniTest::Test
6
+
7
+ describe Clickhouse::Cluster do
8
+ it "creates a connection pond" do
9
+ cluster = Clickhouse::Cluster.new :urls => %w(localhost:1234 localhost:1235 localhost:1236)
10
+ assert_equal true, cluster.pond.is_a?(Pond)
11
+ end
12
+
13
+ it "does not modify the passed config" do
14
+ config = {:urls => %w(localhost:1234 localhost:1235 localhost:1236)}
15
+ Clickhouse::Cluster.new config
16
+ assert_equal({:urls => %w(http://localhost:1234 http://localhost:1235 http://localhost:1236)}, config)
17
+ end
18
+
19
+ describe "when connection succeeds" do
20
+ it "keeps valid connections from the pond" do
21
+ Clickhouse::Connection.any_instance.expects(:tables)
22
+ Clickhouse::Connection.any_instance.expects(:ping!)
23
+
24
+ cluster = Clickhouse::Cluster.new :urls => %w(http://localhost:1234 http://localhost:1235 http://localhost:1236)
25
+ assert_equal %w(
26
+ http://localhost:1234
27
+ http://localhost:1235
28
+ http://localhost:1236
29
+ ), cluster.pond.available.collect(&:url)
30
+
31
+ cluster.tables
32
+ assert_equal %w(
33
+ http://localhost:1235
34
+ http://localhost:1236
35
+ http://localhost:1234
36
+ ), cluster.pond.available.collect(&:url)
37
+ end
38
+ end
39
+
40
+ describe "when connection fails" do
41
+ it "removes invalid connections from the pond" do
42
+ cluster = Clickhouse::Cluster.new :urls => %w(http://localhost:1234 http://localhost:1235 http://localhost:1236)
43
+
44
+ assert_equal %w(
45
+ http://localhost:1234
46
+ http://localhost:1235
47
+ http://localhost:1236
48
+ ), cluster.pond.available.collect(&:url)
49
+
50
+ cluster.tables
51
+ assert_equal [], cluster.pond.available.collect(&:url)
52
+ end
53
+ end
54
+
55
+ describe "when error gets raised other than Clickhouse::ConnectionError" do
56
+ it "does not remove the connection from the pond" do
57
+ Clickhouse::Connection.any_instance.expects(:ping!)
58
+
59
+ cluster = Clickhouse::Cluster.new :urls => %w(http://localhost:1234 http://localhost:1235 http://localhost:1236)
60
+ assert_equal %w(
61
+ http://localhost:1234
62
+ http://localhost:1235
63
+ http://localhost:1236
64
+ ), cluster.pond.available.collect(&:url)
65
+
66
+ assert_raises NoMethodError do
67
+ cluster.select_rows ""
68
+ end
69
+
70
+ assert_equal %w(
71
+ http://localhost:1235
72
+ http://localhost:1236
73
+ http://localhost:1234
74
+ ), cluster.pond.available.collect(&:url)
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ 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(:debug, "Hello world!")
20
+ Clickhouse.expects(:logger).returns(logger).twice
21
+ @connection.send(:log, :debug, "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, :debug, "Boo!")
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,410 @@
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
+ @connection.stubs(:parse_stats)
16
+ @connection.stubs(:write_log)
17
+ end
18
+
19
+ describe "#execute" do
20
+ it "sends a POST request" do
21
+ @connection.expects(:post).with("sql", nil).returns("")
22
+ assert_equal true, @connection.execute("sql")
23
+ end
24
+
25
+ describe "when server returns a non-empty body" do
26
+ it "returns the body of the response" do
27
+ @connection.expects(:post).with("sql", "body").returns("Ok.")
28
+ assert_equal "Ok.", @connection.execute("sql", "body")
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#query" do
34
+ it "sends a GET request requesting a TSV response including names and types" do
35
+ @connection.expects(:get).with("sql FORMAT JSONCompact")
36
+ @connection.stubs(:parse_data)
37
+ assert_equal [], @connection.query("sql").to_a
38
+ end
39
+ end
40
+
41
+ describe "#databases" do
42
+ it "sends a 'SHOW DATABASES' query" do
43
+ @connection.expects(:get).with("SHOW DATABASES FORMAT JSONCompact")
44
+ @connection.stubs(:parse_data).returns([])
45
+ assert_equal [], @connection.databases
46
+ end
47
+ end
48
+
49
+ describe "#tables" do
50
+ it "sends a 'SHOW TABLES' query" do
51
+ @connection.expects(:get).with("SHOW TABLES FORMAT JSONCompact")
52
+ @connection.stubs(:parse_data).returns([])
53
+ @connection.tables
54
+ end
55
+ end
56
+
57
+ describe "#create_table" do
58
+ it "sends a 'CREATE TABLE' query" do
59
+ sql = <<-SQL
60
+ CREATE TABLE logs_test (
61
+ id UInt8,
62
+ price Float32,
63
+ name String,
64
+ date Date,
65
+ time DateTime,
66
+ hex_id FixedString(8)
67
+ )
68
+ ENGINE = MergeTree(date, 8192)
69
+ SQL
70
+ @connection.expects(:post).with(sql.strip, nil).returns("")
71
+ @connection.create_table("logs_test") do |t|
72
+ t.uint8 :id
73
+ t.float32 :price
74
+ t.string :name
75
+ t.date :date
76
+ t.date_time :time
77
+ t.fixed_string :hex_id, 8
78
+ t.engine "MergeTree(date, 8192)"
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "#describe_table" do
84
+ it "sends a 'DESCRIBE TABLE <name>' query" do
85
+ @connection.expects(:get).with("DESCRIBE TABLE logs FORMAT JSONCompact")
86
+ @connection.stubs(:parse_data)
87
+ @connection.describe_table("logs")
88
+ end
89
+ end
90
+
91
+ describe "#rename_table" do
92
+ describe "when passing an array with an even number of names" do
93
+ it "sends a POST request containing a RENAME TABLE statement" do
94
+ @connection.expects(:post).with("RENAME TABLE foo TO bar, baz TO qux", nil).returns("").twice
95
+ assert_equal true, @connection.rename_table("foo", "bar", "baz", "qux")
96
+ assert_equal true, @connection.rename_table(["foo", "bar"], ["baz", "qux"])
97
+ end
98
+ end
99
+
100
+ describe "when passing an array with an odd number of names" do
101
+ it "raises an Clickhouse::InvalidQueryError" do
102
+ assert_raises Clickhouse::InvalidQueryError do
103
+ @connection.rename_table "foo"
104
+ end
105
+ assert_raises Clickhouse::InvalidQueryError do
106
+ @connection.rename_table ["foo"]
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "when passing a hash" do
112
+ it "sends a POST request containing a RENAME TABLE statement" do
113
+ @connection.expects(:post).with("RENAME TABLE foo TO bar, baz TO qux", nil).returns("")
114
+ assert_equal true, @connection.rename_table(:foo => "bar", :baz => "qux")
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "#drop_table" do
120
+ it "sends a POST request containing a 'DROP TABLE' statement" do
121
+ @connection.expects(:post).with("DROP TABLE logs", nil).returns("")
122
+ assert_equal true, @connection.drop_table("logs")
123
+ end
124
+ end
125
+
126
+ describe "#insert_rows" do
127
+ before do
128
+ @csv = <<-CSV
129
+ id,first_name,last_name
130
+ 12345,Paul,Engel
131
+ 67890,Bruce,Wayne
132
+ CSV
133
+ @csv.gsub!(/^\s+/, "")
134
+ end
135
+
136
+ describe "when using hashes" do
137
+ it "sends a POST request containing a 'INSERT INTO' statement using CSV" do
138
+ @connection.expects(:post).with("INSERT INTO logs FORMAT CSVWithNames", @csv).returns("")
139
+ assert_equal true, @connection.insert_rows("logs") { |rows|
140
+ rows << {:id => 12345, :first_name => "Paul", :last_name => "Engel"}
141
+ rows << {:id => 67890, :first_name => "Bruce", :last_name => "Wayne"}
142
+ }
143
+ end
144
+ end
145
+
146
+ describe "when using arrays" do
147
+ it "sends a POST request containing a 'INSERT INTO' statement using CSV" do
148
+ @connection.expects(:post).with("INSERT INTO logs FORMAT CSVWithNames", @csv).returns("")
149
+ assert_equal true, @connection.insert_rows("logs", :names => %w(id first_name last_name)) { |rows|
150
+ rows << [12345, "Paul", "Engel"]
151
+ rows << [67890, "Bruce", "Wayne"]
152
+ }
153
+ end
154
+ end
155
+ end
156
+
157
+ describe "#select_rows" do
158
+ it "sends a GET request and parses the result set" do
159
+ body = <<-JAVASCRIPT
160
+ {
161
+ "meta": [
162
+ {"name": "year", "type": "UInt16"},
163
+ {"name": "name", "type": "String"}
164
+ ],
165
+ "data": [
166
+ [1982, "Paul"],
167
+ [1947, "Anna"]
168
+ ]
169
+ }
170
+ JAVASCRIPT
171
+
172
+ @connection.expects(:to_select_query).with(options = {:from => "logs"}).returns("")
173
+ @connection.expects(:get).returns(JSON.parse(body))
174
+
175
+ assert_equal [
176
+ [1982, "Paul"],
177
+ [1947, "Anna"]
178
+ ], @connection.select_rows(options).to_a
179
+ end
180
+ end
181
+
182
+ describe "#select_row" do
183
+ it "returns an empty array" do
184
+ @connection.expects(:select_rows).returns([["Paul", "Engel"], ["Bruce", "Wayne"]])
185
+ assert_equal ["Paul", "Engel"], @connection.select_row({})
186
+ end
187
+ end
188
+
189
+ describe "#select_values" do
190
+ describe "when empty result set" do
191
+ it "returns an empty array" do
192
+ @connection.expects(:to_select_query).returns("")
193
+ @connection.expects(:get).returns(stub(:body => ""))
194
+ @connection.stubs(:parse_data).returns([])
195
+ assert_equal [], @connection.select_values({})
196
+ end
197
+ end
198
+
199
+ describe "when getting data" do
200
+ it "returns every first value of every row" do
201
+ body = <<-JAVASCRIPT
202
+ {
203
+ "meta": [
204
+ {"name": "year", "type": "UInt16"},
205
+ {"name": "name", "type": "String"}
206
+ ],
207
+ "data": [
208
+ [1982, "Paul"],
209
+ [1947, "Anna"]
210
+ ]
211
+ }
212
+ JAVASCRIPT
213
+
214
+ @connection.expects(:to_select_query).returns("")
215
+ @connection.expects(:get).returns(JSON.parse(body))
216
+ assert_equal [
217
+ 1982,
218
+ 1947
219
+ ], @connection.select_values({})
220
+ end
221
+ end
222
+ end
223
+
224
+ describe "#select_value" do
225
+ describe "when empty result set" do
226
+ it "returns nil" do
227
+ @connection.expects(:select_values).with(options = {:foo => "bar"}).returns([])
228
+ assert_nil @connection.select_value(options)
229
+ end
230
+ end
231
+
232
+ describe "when getting data" do
233
+ it "returns the first value of the first row" do
234
+ @connection.expects(:select_values).with(options = {:foo => "bar"}).returns([1982])
235
+ assert_equal 1982, @connection.select_value(options)
236
+ end
237
+ end
238
+ end
239
+
240
+ describe "#count" do
241
+ it "returns the first value of the first row" do
242
+ @connection.expects(:select_value).with(:select => "COUNT(*)", :from => "logs").returns(1982)
243
+ assert_equal 1982, @connection.count(:from => "logs")
244
+ end
245
+ end
246
+
247
+ describe "#to_select_query" do
248
+ describe "when passing :from option" do
249
+ it "generates a simple 'SELECT * FROM <table>' query" do
250
+ query = <<-SQL
251
+ SELECT *
252
+ FROM logs
253
+ SQL
254
+ options = {
255
+ :from => "logs"
256
+ }
257
+ assert_query(query, @connection.send(:to_select_query, options))
258
+ end
259
+ end
260
+
261
+ describe "when passing :from and :select option" do
262
+ describe "when passing a single column" do
263
+ it "respects the single column in the SELECT statement" do
264
+ query = <<-SQL
265
+ SELECT MIN(date)
266
+ FROM logs
267
+ SQL
268
+ options = {
269
+ :select => "MIN(date)",
270
+ :from => "logs"
271
+ }
272
+ assert_query(query, @connection.send(:to_select_query, options))
273
+ end
274
+ end
275
+
276
+ describe "when passing multiple columns" do
277
+ it "only includes the passed columns in the SELECT statement" do
278
+ query = <<-SQL
279
+ SELECT MIN(date), MAX(date)
280
+ FROM logs
281
+ SQL
282
+ options = {
283
+ :select => ["MIN(date)", "MAX(date)"],
284
+ :from => "logs"
285
+ }
286
+ assert_query(query, @connection.send(:to_select_query, options))
287
+ end
288
+ end
289
+
290
+ describe "when filtering on value is empty" do
291
+ it "uses the empty() function in the WHERE statement" do
292
+ query = <<-SQL
293
+ SELECT *
294
+ FROM logs
295
+ WHERE empty(parent_id)
296
+ SQL
297
+ options = {
298
+ :from => "logs",
299
+ :where => {
300
+ :parent_id => :empty
301
+ }
302
+ }
303
+ assert_query(query, @connection.send(:to_select_query, options))
304
+ end
305
+ end
306
+
307
+ describe "when filtering on value is within a certain range" do
308
+ it "includes the range in the WHERE statement" do
309
+ query = <<-SQL
310
+ SELECT *
311
+ FROM logs
312
+ WHERE code >= 6 AND code <= 10
313
+ SQL
314
+ options = {
315
+ :from => "logs",
316
+ :where => {
317
+ :code => 6..10
318
+ }
319
+ }
320
+ assert_query(query, @connection.send(:to_select_query, options))
321
+ end
322
+ end
323
+
324
+ describe "when filtering on value in array" do
325
+ it "uses an IN operator in the WHERE statement" do
326
+ query = <<-SQL
327
+ SELECT *
328
+ FROM logs
329
+ WHERE code IN (6, 7, 8, 9, 10)
330
+ SQL
331
+ options = {
332
+ :from => "logs",
333
+ :where => {
334
+ :code => [6, 7, 8, 9, 10]
335
+ }
336
+ }
337
+ assert_query(query, @connection.send(:to_select_query, options))
338
+ end
339
+ end
340
+
341
+ describe "when filtering using backticks" do
342
+ it "uses the specified SQL as is" do
343
+ query = <<-SQL
344
+ SELECT *
345
+ FROM logs
346
+ WHERE id != 'cb5a67d2932911e6'
347
+ SQL
348
+ options = {
349
+ :from => "logs",
350
+ :where => {
351
+ :id => "`!= 'cb5a67d2932911e6'`"
352
+ }
353
+ }
354
+ assert_query(query, @connection.send(:to_select_query, options))
355
+ end
356
+ end
357
+
358
+ describe "when filtering on a string" do
359
+ it "uses a single quoted string" do
360
+ query = <<-SQL
361
+ SELECT *
362
+ FROM logs
363
+ WHERE id = 'cb5a67d2932911e6'
364
+ SQL
365
+ options = {
366
+ :from => "logs",
367
+ :where => {
368
+ :id => "cb5a67d2932911e6"
369
+ }
370
+ }
371
+ assert_query(query, @connection.send(:to_select_query, options))
372
+ end
373
+ end
374
+
375
+ describe "when using all options" do
376
+ it "generates the complex query" do
377
+ query = <<-SQL
378
+ SELECT date, COUNT(id), groupUniqArray(severity), SUM(clicks)
379
+ FROM logs
380
+ WHERE date >= '2016-08-01' AND hidden = 0
381
+ GROUP BY date
382
+ HAVING MIN(severity) = 2
383
+ ORDER BY MIN(time) DESC
384
+ LIMIT 120, 60
385
+ SQL
386
+ options = {
387
+ :select => ["date", "COUNT(id)", "groupUniqArray(severity)", "SUM(clicks)"],
388
+ :from => "logs",
389
+ :where => {
390
+ :date => "`>= '2016-08-01'`",
391
+ :hidden => 0
392
+ },
393
+ :group => "date",
394
+ :having => {
395
+ "MIN(severity)" => 2
396
+ },
397
+ :order => "MIN(time) DESC",
398
+ :limit => 60,
399
+ :offset => 120
400
+ }
401
+ assert_query(query, @connection.send(:to_select_query, options))
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ end
409
+ end
410
+ end