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,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
+ }