ch-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +22 -0
- data/.gitignore +9 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +262 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/bin/clickhouse +9 -0
- data/clickhouse.gemspec +36 -0
- data/lib/clickhouse.rb +60 -0
- data/lib/clickhouse/cli.rb +46 -0
- data/lib/clickhouse/cli/client.rb +149 -0
- data/lib/clickhouse/cli/console.rb +73 -0
- data/lib/clickhouse/cli/server.rb +37 -0
- data/lib/clickhouse/cli/server/assets/css/clickhouse.css +177 -0
- data/lib/clickhouse/cli/server/assets/css/codemirror.css +341 -0
- data/lib/clickhouse/cli/server/assets/css/datatables.css +1 -0
- data/lib/clickhouse/cli/server/assets/css/normalize.css +427 -0
- data/lib/clickhouse/cli/server/assets/css/skeleton.css +418 -0
- data/lib/clickhouse/cli/server/assets/js/clickhouse.js +188 -0
- data/lib/clickhouse/cli/server/assets/js/codemirror.js +9096 -0
- data/lib/clickhouse/cli/server/assets/js/datatables.js +166 -0
- data/lib/clickhouse/cli/server/assets/js/disableswipeback.js +97 -0
- data/lib/clickhouse/cli/server/assets/js/jquery.js +11015 -0
- data/lib/clickhouse/cli/server/assets/js/sql.js +232 -0
- data/lib/clickhouse/cli/server/views/index.erb +46 -0
- data/lib/clickhouse/cluster.rb +43 -0
- data/lib/clickhouse/connection.rb +42 -0
- data/lib/clickhouse/connection/client.rb +135 -0
- data/lib/clickhouse/connection/logger.rb +12 -0
- data/lib/clickhouse/connection/query.rb +160 -0
- data/lib/clickhouse/connection/query/result_row.rb +36 -0
- data/lib/clickhouse/connection/query/result_set.rb +103 -0
- data/lib/clickhouse/connection/query/table.rb +50 -0
- data/lib/clickhouse/error.rb +18 -0
- data/lib/clickhouse/utils.rb +23 -0
- data/lib/clickhouse/version.rb +7 -0
- data/script/console +58 -0
- data/test/test_helper.rb +15 -0
- data/test/test_helper/coverage.rb +16 -0
- data/test/test_helper/minitest.rb +13 -0
- data/test/test_helper/simple_connection.rb +12 -0
- data/test/unit/connection/query/test_result_row.rb +36 -0
- data/test/unit/connection/query/test_result_set.rb +196 -0
- data/test/unit/connection/query/test_table.rb +39 -0
- data/test/unit/connection/test_client.rb +206 -0
- data/test/unit/connection/test_cluster.rb +81 -0
- data/test/unit/connection/test_logger.rb +35 -0
- data/test/unit/connection/test_query.rb +410 -0
- data/test/unit/test_clickhouse.rb +99 -0
- data/test/unit/test_connection.rb +55 -0
- data/test/unit/test_utils.rb +39 -0
- metadata +326 -0
data/lib/clickhouse.rb
ADDED
@@ -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
|
+
}
|