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,15 @@
1
+ require_relative "test_helper/coverage"
2
+
3
+ require "minitest"
4
+ require "minitest/autorun"
5
+ require "mocha/setup"
6
+
7
+ def path(path)
8
+ File.expand_path "../../#{path}", __FILE__
9
+ end
10
+
11
+ require "bundler"
12
+ Bundler.require :default, :development, :test
13
+
14
+ require_relative "test_helper/minitest"
15
+ require_relative "test_helper/simple_connection"
@@ -0,0 +1,16 @@
1
+ if Dir.pwd == File.expand_path("../../..", __FILE__)
2
+ if ENV["REPORT"].to_i == 1
3
+ require "dotenv"
4
+ Dotenv.load
5
+
6
+ require "codeclimate-test-reporter"
7
+ CodeClimate::TestReporter.start
8
+ end
9
+
10
+ require "simplecov"
11
+ SimpleCov.coverage_dir "test/coverage"
12
+ SimpleCov.start do
13
+ add_group "Clickhouse", "lib"
14
+ add_group "Test suite", "test"
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ class MiniTest::Test
2
+ def teardown
3
+ Clickhouse.instance_variables.each do |name|
4
+ Clickhouse.instance_variable_set name, nil
5
+ end
6
+ end
7
+ end
8
+
9
+ class MiniTest::Spec
10
+ def assert_query(expected, actual)
11
+ assert_equal(expected.strip.gsub(/^\s+/, ""), actual)
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ class SimpleConnection
2
+ attr_reader :config
3
+
4
+ def initialize
5
+ @config = {
6
+ :scheme => "http",
7
+ :host => "localhost",
8
+ :port => 8123
9
+ }
10
+ end
11
+
12
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "../../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ module Query
6
+ class TestResultRow < MiniTest::Test
7
+
8
+ describe Clickhouse::Connection::Query::ResultRow do
9
+ describe "#to_hash" do
10
+ describe "when passing names" do
11
+ it "uses the names as hash keys" do
12
+ result_row = Clickhouse::Connection::Query::ResultRow.new([1, 2, 3], [:a, :b, :c])
13
+ assert_equal({:a => 1, :b => 2, :c => 3}, result_row.to_hash)
14
+ end
15
+ end
16
+
17
+ describe "when not passing names" do
18
+ it "uses 'column<i>' as hash keys" do
19
+ result_row = Clickhouse::Connection::Query::ResultRow.new([1, 2, 3])
20
+ assert_equal({"column0" => 1, "column1" => 2, "column2" => 3}, result_row.to_hash)
21
+ end
22
+ end
23
+
24
+ describe "memoization" do
25
+ it "memoizes the resulting hash" do
26
+ result_row = Clickhouse::Connection::Query::ResultRow.new([1, 2, 3])
27
+ assert_equal result_row.to_hash.object_id, result_row.to_hash.object_id
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,196 @@
1
+ require_relative "../../../test_helper"
2
+
3
+ module Unit
4
+ module Connection
5
+ module Query
6
+ class TestResultSet < MiniTest::Test
7
+
8
+ describe Clickhouse::Connection::Query::ResultSet do
9
+ before do
10
+ @empty_set = Clickhouse::Connection::Query::ResultSet.new
11
+ @result_set = Clickhouse::Connection::Query::ResultSet.new(
12
+ [
13
+ [
14
+ "1072649",
15
+ "142.94",
16
+ "badrequest.io",
17
+ "d91d1c90\u0000\u0000\u0000",
18
+ "2016-03-20",
19
+ "2016-03-20 23:49:11",
20
+ [4, 2, 5, 7]
21
+ ], [
22
+ "12948140",
23
+ "9320.11",
24
+ "engel.pm",
25
+ "d91d217c\u0000\u0000",
26
+ "2016-03-20",
27
+ "2016-03-20 23:58:34",
28
+ [6, 2, 9, 8, 1]
29
+ ], [
30
+ "319384",
31
+ "101.02",
32
+ "archan937.com",
33
+ "d91d2294\u0000\u0000\u0000",
34
+ "2016-03-20",
35
+ "2016-03-20 22:55:39",
36
+ [3, 1, 2]
37
+ ]
38
+ ],
39
+ %w(
40
+ SUM(clicks)
41
+ AVG(price)
42
+ domain
43
+ id
44
+ date
45
+ MAX(time)
46
+ groupUniqArray(code)
47
+ ),
48
+ %w(
49
+ UInt32
50
+ Float32
51
+ String
52
+ FixedString(16)
53
+ Date
54
+ DateTime
55
+ Array(8)
56
+ )
57
+ )
58
+ end
59
+
60
+ describe "#size" do
61
+ it "returns the size of the result set" do
62
+ assert_equal 3, @result_set.size
63
+ end
64
+ end
65
+
66
+ describe "#empty?" do
67
+ it "returns whether the result set is empty or not" do
68
+ assert_equal true, @empty_set.empty?
69
+ assert_equal false, @result_set.empty?
70
+ end
71
+ end
72
+
73
+ describe "#present?" do
74
+ it "returns whether the result set contains rows or not" do
75
+ assert_equal false, @empty_set.present?
76
+ assert_equal true, @result_set.present?
77
+ end
78
+ end
79
+
80
+ describe "#first" do
81
+ it "returns the first row of the result set" do
82
+ assert_equal [
83
+ 1072649,
84
+ 142.94,
85
+ "badrequest.io",
86
+ "d91d1c90",
87
+ Date.new(2016, 3, 20),
88
+ Time.new(2016, 3, 20, 23, 49, 11),
89
+ [4, 2, 5, 7]
90
+ ], @result_set.first
91
+ end
92
+ end
93
+
94
+ describe "#last" do
95
+ it "returns the size of the result set" do
96
+ assert_equal [
97
+ 319384,
98
+ 101.02,
99
+ "archan937.com",
100
+ "d91d2294",
101
+ Date.new(2016, 3, 20),
102
+ Time.new(2016, 3, 20, 22, 55, 39),
103
+ [3, 1, 2]
104
+ ], @result_set.last
105
+ end
106
+ end
107
+
108
+ describe "#flatten" do
109
+ it "returns the size of the result set" do
110
+ assert_equal [
111
+ 1072649,
112
+ 142.94,
113
+ "badrequest.io",
114
+ "d91d1c90",
115
+ Date.new(2016, 3, 20),
116
+ Time.new(2016, 3, 20, 23, 49, 11),
117
+ 4,
118
+ 2,
119
+ 5,
120
+ 7,
121
+ 12948140,
122
+ 9320.11,
123
+ "engel.pm",
124
+ "d91d217c",
125
+ Date.new(2016, 3, 20),
126
+ Time.new(2016, 3, 20, 23, 58, 34),
127
+ 6,
128
+ 2,
129
+ 9,
130
+ 8,
131
+ 1,
132
+ 319384,
133
+ 101.02,
134
+ "archan937.com",
135
+ "d91d2294",
136
+ Date.new(2016, 3, 20),
137
+ Time.new(2016, 3, 20, 22, 55, 39),
138
+ 3,
139
+ 1,
140
+ 2
141
+ ], @result_set.flatten
142
+ end
143
+ end
144
+
145
+ describe "#to_hashes" do
146
+ it "returns an array containing the rows as hashes" do
147
+ assert_equal [
148
+ {
149
+ "SUM(clicks)" => 1072649,
150
+ "AVG(price)" => 142.94,
151
+ "domain" => "badrequest.io",
152
+ "id" => "d91d1c90",
153
+ "date" => Date.new(2016, 3, 20),
154
+ "MAX(time)" => Time.new(2016, 3, 20, 23, 49, 11),
155
+ "groupUniqArray(code)" => [4, 2, 5, 7]
156
+ }, {
157
+ "SUM(clicks)" => 12948140,
158
+ "AVG(price)" => 9320.11,
159
+ "domain" => "engel.pm",
160
+ "id" => "d91d217c",
161
+ "date" => Date.new(2016, 3, 20),
162
+ "MAX(time)" => Time.new(2016, 3, 20, 23, 58, 34),
163
+ "groupUniqArray(code)" => [6, 2, 9, 8, 1]
164
+ }, {
165
+ "SUM(clicks)" => 319384,
166
+ "AVG(price)" => 101.02,
167
+ "domain" => "archan937.com",
168
+ "id" => "d91d2294",
169
+ "date" => Date.new(2016, 3, 20),
170
+ "MAX(time)" => Time.new(2016, 3, 20, 22, 55, 39),
171
+ "groupUniqArray(code)" => [3, 1, 2]
172
+ }
173
+ ], @result_set.to_hashes
174
+ end
175
+ end
176
+
177
+ describe "memoization" do
178
+ it "memoizes the parsed rows" do
179
+ assert_equal @result_set.to_a[-1].object_id, @result_set.each{}[-1].object_id
180
+ assert_equal @result_set.first.object_id, @result_set[0].object_id
181
+ end
182
+ end
183
+
184
+ describe "non-supported data types" do
185
+ it "raises a NotImplementedError error" do
186
+ assert_raises NotImplementedError do
187
+ Clickhouse::Connection::Query::ResultSet.new([[1]], ["Foo"], ["Bar"])[0]
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ end
194
+ end
195
+ end
196
+ end
@@ -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,206 @@
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
+ @connection.stubs(:parse_stats)
15
+ @connection.stubs(:write_log)
16
+ end
17
+
18
+ describe "#connect!" do
19
+ describe "when failed to connect" do
20
+ it "returns true" do
21
+ Faraday::Connection.any_instance.expects(:get).raises(Faraday::ConnectionFailed.new("Failed to connect"))
22
+ assert_raises Clickhouse::ConnectionError do
23
+ @connection.connect!
24
+ end
25
+ end
26
+ end
27
+
28
+ describe "when receiving 200" do
29
+ it "returns true" do
30
+ Faraday::Connection.any_instance.expects(:get).returns(stub(:status => 200))
31
+ assert_equal true, @connection.connect!
32
+ end
33
+ end
34
+
35
+ describe "when receiving 500" do
36
+ it "raises a Clickhouse::ConnectionError" do
37
+ Faraday::Connection.any_instance.expects(:get).returns(stub(:status => 500))
38
+ assert_raises Clickhouse::ConnectionError do
39
+ @connection.connect!
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "when already connected" do
45
+ it "returns nil" do
46
+ @connection.instance_variable_set :@client, mock
47
+ assert_nil @connection.connect!
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#connected?" do
53
+ it "returns whether it has an connected socket" do
54
+ assert_equal false, @connection.connected?
55
+ @connection.instance_variable_set :@client, mock
56
+ assert_equal true, @connection.connected?
57
+ @connection.instance_variable_set :@client, nil
58
+ assert_equal false, @connection.connected?
59
+ end
60
+ end
61
+
62
+ describe "#get" do
63
+ it "sends a GET request the server" do
64
+ @connection.instance_variable_set :@client, (client = mock)
65
+ client.expects(:get).with("/?query=foo&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => ""))
66
+ @connection.stubs(:log)
67
+ @connection.get("foo")
68
+ end
69
+ end
70
+
71
+ describe "#post" do
72
+ it "sends a POST request the server" do
73
+ @connection.instance_variable_set :@client, (client = mock)
74
+ client.expects(:post).with("/?query=foo&output_format_write_statistics=1", "body").returns(stub(:status => 200, :body => ""))
75
+ @connection.stubs(:log)
76
+ @connection.post("foo", "body")
77
+ end
78
+ end
79
+
80
+ describe "#request" do
81
+ before do
82
+ @connection.stubs(:log)
83
+ end
84
+
85
+ it "connects to the server first" do
86
+ @connection.instance_variable_set :@client, (client = mock)
87
+ @connection.expects(:connect!)
88
+ client.stubs(:get).returns(stub(:status => 200, :body => ""))
89
+ @connection.send :request, :get, "/", "query"
90
+ end
91
+
92
+ it "queries the server returning the response" do
93
+ @connection.instance_variable_set :@client, (client = mock)
94
+ client.expects(:get).with("/?query=SELECT+1&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => ""))
95
+ @connection.expects(:parse_body).returns(data = mock)
96
+ assert_equal data, @connection.send(:request, :get, "SELECT 1")
97
+ end
98
+
99
+ describe "when not receiving status 200" do
100
+ it "raises a Clickhouse::QueryError" do
101
+ @connection.instance_variable_set :@client, (client = mock)
102
+ client.expects(:get).with("/?query=SELECT+1&output_format_write_statistics=1", nil).returns(stub(:status => 500, :body => ""))
103
+ assert_raises Clickhouse::QueryError do
104
+ @connection.send(:request, :get, "SELECT 1")
105
+ end
106
+ end
107
+ end
108
+
109
+ describe "when getting Faraday::Error" do
110
+ it "raises a Clickhouse::ConnectionError" do
111
+ @connection.instance_variable_set :@client, (client = mock)
112
+ client.expects(:get).raises(Faraday::ConnectionFailed.new("Failed to connect"))
113
+ assert_raises Clickhouse::ConnectionError do
114
+ @connection.send(:request, :get, "SELECT 1")
115
+ end
116
+ end
117
+ end
118
+
119
+ it "parses the body" do
120
+ json = <<-JSON
121
+ {"meta": []}
122
+ JSON
123
+ @connection.instance_variable_set :@client, (client = mock)
124
+ client.expects(:get).with("/?query=SELECT+1+FORMAT+JSONCompact&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => json))
125
+ assert_equal({"meta" => []}, @connection.send(:request, :get, "SELECT 1 FORMAT JSONCompact"))
126
+ end
127
+ end
128
+
129
+ describe "configuration" do
130
+ describe "database" do
131
+ it "includes the database in the querystring" do
132
+ @connection.instance_variable_get(:@config)[:database] = "system"
133
+ @connection.instance_variable_set(:@client, (client = mock))
134
+ client.expects(:get).with("/?database=system&query=SELECT+1&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => ""))
135
+ @connection.expects(:parse_body).returns(data = mock)
136
+ assert_equal data, @connection.send(:request, :get, "SELECT 1")
137
+ end
138
+ end
139
+
140
+ describe "authentication" do
141
+ it "includes the credentials in the request headers" do
142
+ Faraday::Connection.any_instance.expects(:get).returns(stub(status: 200))
143
+ connection = Clickhouse::Connection.new :password => "awesomepassword"
144
+ connection.connect!
145
+ assert_equal "Basic ZGVmYXVsdDphd2Vzb21lcGFzc3dvcmQ=", connection.send(:client).headers["Authorization"].force_encoding("UTF-8")
146
+ end
147
+ end
148
+ end
149
+
150
+ describe "statistics" do
151
+ before do
152
+ @connection = Connection.new
153
+ @json = <<-JSON
154
+ {
155
+ "rows": 1947,
156
+ "statistics": {
157
+ "elapsed": 0.1882,
158
+ "rows_read": 1982,
159
+ "bytes_read": 2003
160
+ }
161
+ }
162
+ JSON
163
+ end
164
+
165
+ it "parses the statistics" do
166
+ @connection.stubs(:log)
167
+ @connection.instance_variable_set :@client, (client = mock)
168
+ Time.expects(:now).returns(1882).twice
169
+
170
+ client.expects(:get).with("/?query=SELECT+1+FORMAT+JSONCompact&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => @json))
171
+ @connection.expects(:write_log).with(
172
+ 0, "SELECT 1", {
173
+ "elapsed" => "188.2ms",
174
+ "rows_read" => "1.98 thousand",
175
+ "bytes_read" => 2003,
176
+ "rows" => 1947,
177
+ "rows_per_second" => "10.53 thousand",
178
+ "data_per_second" => "10.39 KB",
179
+ "data_read" => "1.96 KB"
180
+ }
181
+ )
182
+ @connection.send(:request, :get, "SELECT 1 FORMAT JSONCompact")
183
+ end
184
+
185
+ it "write the expected logs" do
186
+ @connection.instance_variable_set :@client, (client = mock)
187
+ Time.expects(:now).returns(1882).twice
188
+
189
+ client.expects(:get).with("/?query=SELECT+1+FORMAT+JSONCompact&output_format_write_statistics=1", nil).returns(stub(:status => 200, :body => @json))
190
+ log = "\n \e[1m\e[35mSQL (0.0ms)\e\e[0m SELECT 1;\e\n \e[1m\e[36m1947 rows in set. Elapsed: 188.2ms. Processed: 1.98 thousand rows, 1.96 KB (10.53 thousand rows/s, 10.39 KB/s)\e[0m "
191
+
192
+ @connection.expects(:log).with(:debug, log)
193
+ @connection.send(:request, :get, "SELECT 1 FORMAT JSONCompact")
194
+ end
195
+
196
+ describe "#number_to_human_duration" do
197
+ it "returns in seconds when more than 1 seconds" do
198
+ assert_equal "2.0s", @connection.send(:number_to_human_duration, 2)
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ end
205
+ end
206
+ end