ch-client 0.0.1

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.
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