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,60 @@
1
+ require "uri"
2
+ require "forwardable"
3
+ require "csv"
4
+ require "json"
5
+ require "time"
6
+
7
+ require "faraday"
8
+ require "pond"
9
+ require "active_support/dependencies/autoload"
10
+ require "active_support/number_helper"
11
+ require "active_support/core_ext/string/inflections"
12
+
13
+ require "clickhouse/cluster"
14
+ require "clickhouse/connection"
15
+ require "clickhouse/utils"
16
+ require "clickhouse/error"
17
+ require "clickhouse/version"
18
+
19
+ module Clickhouse
20
+
21
+ def self.logger=(logger)
22
+ @logger = logger
23
+ end
24
+
25
+ def self.logger
26
+ @logger if instance_variables.include?(:@logger)
27
+ end
28
+
29
+ def self.configurations=(configurations)
30
+ @configurations = configurations.inject({}){|h, (k, v)| h[k.to_s] = v; h}
31
+ end
32
+
33
+ def self.configurations
34
+ @configurations if instance_variables.include?(:@configurations)
35
+ end
36
+
37
+ def self.establish_connection(arg = {})
38
+ config = arg.is_a?(Hash) ? arg : (configurations || {})[arg.to_s]
39
+ if config
40
+ connect!(config)
41
+ else
42
+ raise InvalidConnectionError, "Invalid connection specified: #{arg.inspect}"
43
+ end
44
+ end
45
+
46
+ def self.connect!(config = {})
47
+ @connection = connect(config)
48
+ @connection.connect!
49
+ end
50
+
51
+ def self.connect(config = {})
52
+ klass = (config[:urls] || config["urls"]) ? Cluster : Connection
53
+ klass.new(config)
54
+ end
55
+
56
+ def self.connection
57
+ @connection if instance_variables.include?(:@connection)
58
+ end
59
+
60
+ end
@@ -0,0 +1,46 @@
1
+ require "thor"
2
+ require "launchy"
3
+ require "clickhouse"
4
+
5
+ module Clickhouse
6
+ class CLI < Thor
7
+
8
+ DEFAULT_URLS = "http://localhost:8123"
9
+
10
+ desc "server [HOSTS]", "Start a Sinatra server as ClickHouse client (HOSTS should be comma separated URIs)"
11
+ method_options [:port, "-p"] => 1982, [:username, "-u"] => :string, [:password, "-P"] => :string
12
+ def server(urls = DEFAULT_URLS)
13
+ run! :server, urls, options do
14
+ Launchy.open "http://localhost:#{options[:port]}"
15
+ end
16
+ end
17
+
18
+ desc "console [HOSTS]", "Start a Pry console as ClickHouse client (HOSTS should be comma separated URIs)"
19
+ method_options [:username, "-u"] => :string, [:password, "-P"] => :string
20
+ def console(urls = DEFAULT_URLS)
21
+ run! :console, urls, options
22
+ end
23
+
24
+ map "s" => :server
25
+ map "c" => :console
26
+
27
+ private
28
+
29
+ def run!(const, urls, options, &block)
30
+ require_relative "cli/client"
31
+ require_relative "cli/#{const}"
32
+ connect! urls, options
33
+ self.class.const_get(const.to_s.capitalize).run!(:port => options["port"], &block)
34
+ end
35
+
36
+ def connect!(urls, options)
37
+ config = options.merge(:urls => urls.split(",")).inject({}){|h, (k, v)| h[k.to_sym] = v; h}
38
+ Clickhouse.establish_connection config
39
+ end
40
+
41
+ def method_missing(method, *_args)
42
+ raise Error, "Unrecognized command \"#{method}\". Please consult `clickhouse help`."
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,149 @@
1
+ module Clickhouse
2
+ class CLI < Thor
3
+ module Client
4
+
5
+ HISTORY_FILE = "#{ENV["HOME"]}/.clickhouse_history"
6
+
7
+ def self.included(base)
8
+ extended(base)
9
+ end
10
+
11
+ def self.extended(base)
12
+ Clickhouse.logger = self
13
+ load_history
14
+ end
15
+
16
+ def self.debug(message = nil)
17
+ @log = message.split("\n").detect{|line| line.include?("36m")}
18
+ end
19
+
20
+ def self.log
21
+ @log
22
+ end
23
+
24
+ def self.load_history
25
+ File.readlines(HISTORY_FILE).each do |line|
26
+ Readline::HISTORY.push line.gsub(";;", "\n").strip
27
+ end if File.exists?(HISTORY_FILE)
28
+ end
29
+
30
+ def alter_history(sql, current = true)
31
+ (Readline::HISTORY.to_a.count{|line| line[-1] != ";"} + (current ? 1 : 0)).times do
32
+ Readline::HISTORY.pop
33
+ end
34
+ unless Readline::HISTORY.to_a[-1] == sql
35
+ Readline::HISTORY.push(sql)
36
+ end
37
+ end
38
+
39
+ def dump_history
40
+ File.open(HISTORY_FILE, "w+") do |file|
41
+ Readline::HISTORY.each do |line|
42
+ file.puts line.strip.gsub("\n", ";;")
43
+ end
44
+ end
45
+ end
46
+
47
+ def prettify(sql)
48
+ sql, replaced = numerize_patterns(sql)
49
+
50
+ preserved_words = %w(
51
+ USE
52
+ SHOW
53
+ DATABASES
54
+ TABLES
55
+ PROCESSLIST
56
+ INSERT
57
+ INTO
58
+ FORMAT
59
+ SELECT
60
+ COUNT
61
+ DISTINCT
62
+ SAMPLE
63
+ AS
64
+ FROM
65
+ JOIN
66
+ UNION
67
+ ALL
68
+ PREWHERE
69
+ WHERE
70
+ AND
71
+ OR
72
+ NOT
73
+ IN
74
+ GROUP
75
+ BY
76
+ HAVING
77
+ ORDER
78
+ LIMIT
79
+ CREATE
80
+ DESCRIBE
81
+ ALTER
82
+ RENAME
83
+ DROP
84
+ DETACH
85
+ ATTACH
86
+ TABLE
87
+ VIEW
88
+ PARTITION
89
+ EXISTS
90
+ SET
91
+ OPTIMIZE
92
+ WITH
93
+ TOTALS
94
+ ).sort{|a, b| [b.size, a] <=> [a.size, b]}
95
+
96
+ sql.gsub!(/(\b)(#{preserved_words.join("|")})(\b)/i) do
97
+ "#{$1}#{$2.upcase}#{$3}"
98
+ end
99
+
100
+ interpolate_patterns(sql, replaced)
101
+ end
102
+
103
+ def numerize_patterns(sql, replaced = [])
104
+ sql = sql.gsub(/(["'])(?:(?=(\\?))\2.)*?\1/) do |match|
105
+ replaced << match
106
+ "${#{replaced.size - 1}}"
107
+ end
108
+
109
+ parenthesized = false
110
+
111
+ sql = sql.gsub(/\([^\(\)]*?\)/) do |match|
112
+ parenthesized = true
113
+ replaced << match
114
+ "%{#{replaced.size - 1}}"
115
+ end
116
+
117
+ parenthesized ? numerize_patterns(sql, replaced) : [sql, replaced]
118
+ end
119
+
120
+ def interpolate_patterns(sql, replaced)
121
+ matched = false
122
+
123
+ sql = sql.gsub(/(\$|%)\{(\d+)\}/) do |match|
124
+ matched = true
125
+ replaced[$2.to_i]
126
+ end
127
+
128
+ matched ? interpolate_patterns(sql, replaced) : sql
129
+ end
130
+
131
+ def execute(sql, &block)
132
+ if sql[-1] == ";"
133
+ dump_history
134
+ method = sql.match(/^(SELECT|SHOW|DESCRIBE)/i) ? :query : :execute
135
+ result = Clickhouse.connection.send(method, sql[0..-2])
136
+
137
+ if block_given?
138
+ block.call(result, Client.log)
139
+ else
140
+ process_result(result, Client.log)
141
+ end
142
+ else
143
+ sql
144
+ end
145
+ end
146
+
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,73 @@
1
+ require "readline"
2
+
3
+ module Clickhouse
4
+ class CLI < Thor
5
+ module Console
6
+ extend self
7
+ extend Client
8
+
9
+ CLR = "\r\e[A\e[K"
10
+
11
+ def run!(options = {})
12
+ readline
13
+ end
14
+
15
+ # private
16
+
17
+ def readline(buffer = nil)
18
+ prompt = buffer ? ":-] " : ":) "
19
+ line = Readline.readline(prompt, true)
20
+
21
+ exit! unless line && !%w(exit quit).include?(line = line.strip)
22
+
23
+ line = prettify(line)
24
+ sql = [buffer, line].compact.join("\n").gsub(/\s+;$/, ";")
25
+
26
+ puts "#{CLR}#{prompt}#{line}"
27
+ alter_history(sql)
28
+
29
+ buffer = begin
30
+ execute(sql)
31
+ rescue Clickhouse::Error => e
32
+ puts "ERROR: #{e.message}"
33
+ end
34
+
35
+ readline buffer
36
+ end
37
+
38
+ def process_result(result, log)
39
+ if result.is_a?(Clickhouse::Connection::Query::ResultSet)
40
+ if result.size > 0
41
+ array = [result.names].concat(result.to_a)
42
+ lengths = array.inject([]) do |lengths, row|
43
+ row.each_with_index do |value, index|
44
+ length = value.to_s.strip.length
45
+ lengths[index] = [lengths[index].to_i, length].max
46
+ end
47
+ lengths
48
+ end
49
+ puts
50
+ array.each_with_index do |row, i|
51
+ values = [nil]
52
+ lengths.each_with_index do |length, index|
53
+ values << row[index].to_s.ljust(length, " ")
54
+ end
55
+ values << nil
56
+ separator = (i == 0) ? "+" : "|"
57
+ puts values.join(" #{separator} ")
58
+ end
59
+ end
60
+ else
61
+ puts result == true ? "Ok." : (result || "Fail.")
62
+ end
63
+
64
+ if log
65
+ puts
66
+ puts log.strip
67
+ end
68
+ puts
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,37 @@
1
+ require "sinatra"
2
+ require "erubis"
3
+
4
+ module Clickhouse
5
+ class CLI < Thor
6
+ class Server < Sinatra::Base
7
+ include Client
8
+
9
+ set :views, File.expand_path("../server/views", __FILE__)
10
+ set :public_folder, File.expand_path("../server/assets", __FILE__)
11
+
12
+ get "/" do
13
+ erb :index
14
+ end
15
+
16
+ post "/" do
17
+ sql = prettify(params[:sql]).gsub(/\s+;$/, ";")
18
+ alter_history(sql, false)
19
+ begin
20
+ execute(sql) do |result, log|
21
+ content_type :json
22
+ {
23
+ :urls => Clickhouse.connection.pond.available.collect(&:url),
24
+ :history => Readline::HISTORY.to_a.collect(&:strip),
25
+ :names => result.names,
26
+ :data => result.to_a,
27
+ :stats => log.sub("\e[1m\e[36m", "").sub("\e[0m", "").strip
28
+ }.to_json
29
+ end
30
+ rescue Clickhouse::Error => e
31
+ halt 500, e.message
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,177 @@
1
+ @import url('https://fonts.googleapis.com/css?family=Inconsolata|Open+Sans');
2
+
3
+ body {
4
+ font-family: 'Open Sans', sans-serif;
5
+ font-size: 1.2rem;
6
+ }
7
+
8
+ h1, h2, h3, h4, h5, h6 {
9
+ margin-bottom: 1.1rem;
10
+ }
11
+
12
+ h6 {
13
+ font-size: 1.75rem;
14
+ font-weight: 600;
15
+ }
16
+
17
+ .header {
18
+ padding: 5px 17px;
19
+ background: #F3F3F3;
20
+ border-bottom: 1px solid #E0E0E0;
21
+ }
22
+
23
+ .container {
24
+ width: calc(100% - 36px);
25
+ max-width: inherit;
26
+ }
27
+
28
+ .not_connected {
29
+ color: #D74D2F;
30
+ }
31
+
32
+ small {
33
+ font-size: 1.2rem;
34
+ }
35
+
36
+ a {
37
+ text-decoration: none;
38
+ }
39
+
40
+ a:hover {
41
+ text-decoration: underline;
42
+ }
43
+
44
+ form {
45
+ margin-bottom: 7px;
46
+ }
47
+
48
+ textarea {
49
+ opacity: 0;
50
+ }
51
+
52
+ textarea, .CodeMirror {
53
+ margin-bottom: 1.3em;
54
+ width: 100%;
55
+ height: 9.1em;
56
+ display: block;
57
+ font-family: 'Inconsolata';
58
+ font-size: 1.2rem;
59
+ font-weight: 600;
60
+ line-height: 1.75rem;
61
+ letter-spacing: 0;
62
+ border: 1px solid #DDD;
63
+ }
64
+
65
+ input[type="submit"], a.download {
66
+ height: 30px;
67
+ margin-right: 4px;
68
+ padding: 0 17px 0 20px;
69
+ color: white;
70
+ font-size: 10px;
71
+ line-height: 28px;
72
+ letter-spacing: .05rem;
73
+ border-radius: 0;
74
+ }
75
+
76
+ input[type="submit"] {
77
+ background: #D74D2F;
78
+ border-color: #D74D2F;
79
+ }
80
+
81
+ a.download {
82
+ background: #999;
83
+ border-color: #999;
84
+ }
85
+
86
+ input[type="submit"]:hover, input[type="submit"]:focus {
87
+ color: white;
88
+ border-color: #B30015;
89
+ }
90
+
91
+ a.download:hover, a.download:focus {
92
+ color: white;
93
+ text-decoration: none;
94
+ }
95
+
96
+ input[disabled="disabled"],a[disabled="disabled"] {
97
+ color: #F9F9F9;
98
+ cursor: default !important;
99
+ background: #E4E4E4;
100
+ border-color: #E7E7E7 !important;
101
+ }
102
+
103
+ #stats {
104
+ padding-left: 1.5rem;
105
+ }
106
+
107
+ #result_wrapper {
108
+ margin-bottom: 10px;
109
+ padding-top: 18px;
110
+ overflow-x: auto;
111
+ border-top: 1px solid #DDD;
112
+ }
113
+
114
+ #result_filter {
115
+ float: left;
116
+ }
117
+
118
+ #result_filter label {
119
+ padding-left: 1px;
120
+ font-family: 'Helvetica Neue', 'Arial';
121
+ font-weight: bold;
122
+ }
123
+
124
+ #result_filter input[type="search"] {
125
+ width: 300px;
126
+ height: 28px;
127
+ margin-left: 12px;
128
+ margin-bottom: 12px;
129
+ padding: 6px 7px;
130
+ font-weight: normal;
131
+ border-radius: 0;
132
+ }
133
+
134
+ #result_filter input[type="search"]:focus {
135
+ border-color: #999;
136
+ }
137
+
138
+ #result {
139
+ width: 100% !important;
140
+ margin-bottom: 15px;
141
+ font-family: 'Helvetica Neue', 'Arial';
142
+ font-size: 1.25rem;
143
+ }
144
+
145
+ table#result {
146
+ border: 0;
147
+ border-top: 2px solid #111;
148
+ border-left: 1px solid #DDD;
149
+ }
150
+
151
+ #result th, #result td {
152
+ padding: 4px 8px;
153
+ border: 0;
154
+ border-right: 1px solid #DDD;
155
+ border-bottom: 1px solid #DDD;
156
+ }
157
+
158
+ #result th {
159
+ background: #EEE;
160
+ }
161
+
162
+ #result td {
163
+ white-space: nowrap;
164
+ }
165
+
166
+ #result td.odd {
167
+ background: #DDD;
168
+ }
169
+
170
+ #result td.even {
171
+ background: #FFF;
172
+ }
173
+
174
+ #result_info {
175
+ height: 3px;
176
+ display: none;
177
+ }