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
@@ -0,0 +1,160 @@
|
|
1
|
+
require "clickhouse/connection/query/table"
|
2
|
+
require "clickhouse/connection/query/result_set"
|
3
|
+
require "clickhouse/connection/query/result_row"
|
4
|
+
|
5
|
+
module Clickhouse
|
6
|
+
class Connection
|
7
|
+
module Query
|
8
|
+
|
9
|
+
def execute(query, body = nil)
|
10
|
+
body = post(query, body)
|
11
|
+
body.empty? ? true : body
|
12
|
+
end
|
13
|
+
|
14
|
+
def query(query)
|
15
|
+
query = Utils.extract_format(query)[0]
|
16
|
+
query += " FORMAT JSONCompact"
|
17
|
+
parse_data get(query)
|
18
|
+
end
|
19
|
+
|
20
|
+
def databases
|
21
|
+
query("SHOW DATABASES").flatten
|
22
|
+
end
|
23
|
+
|
24
|
+
def tables
|
25
|
+
query("SHOW TABLES").flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_table(name, &block)
|
29
|
+
execute(Clickhouse::Connection::Query::Table.new(name, &block).to_sql)
|
30
|
+
end
|
31
|
+
|
32
|
+
def describe_table(name)
|
33
|
+
query("DESCRIBE TABLE #{name}").to_a
|
34
|
+
end
|
35
|
+
|
36
|
+
def rename_table(*args)
|
37
|
+
names = (args[0].is_a?(Hash) ? args[0].to_a : [args]).flatten
|
38
|
+
raise Clickhouse::InvalidQueryError, "Odd number of table names" unless (names.size % 2) == 0
|
39
|
+
names = Hash[*names].collect{|(from, to)| "#{from} TO #{to}"}
|
40
|
+
execute("RENAME TABLE #{names.join(", ")}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def drop_table(name)
|
44
|
+
execute("DROP TABLE #{name}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def insert_rows(table, options = {})
|
48
|
+
options[:csv] ||= begin
|
49
|
+
options[:rows] ||= yield([])
|
50
|
+
generate_csv options[:rows], options[:names]
|
51
|
+
end
|
52
|
+
execute("INSERT INTO #{table} FORMAT CSVWithNames", options[:csv])
|
53
|
+
end
|
54
|
+
|
55
|
+
def select_rows(options)
|
56
|
+
query to_select_query(options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def select_row(options)
|
60
|
+
select_rows(options)[0]
|
61
|
+
end
|
62
|
+
|
63
|
+
def select_values(options)
|
64
|
+
select_rows(options).collect{|row| row[0]}
|
65
|
+
end
|
66
|
+
|
67
|
+
def select_value(options)
|
68
|
+
values = select_values(options)
|
69
|
+
values[0] if values
|
70
|
+
end
|
71
|
+
|
72
|
+
def count(options)
|
73
|
+
options = options.merge(:select => "COUNT(*)")
|
74
|
+
select_value(options).to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_select_query(options)
|
78
|
+
to_select_options(options).collect do |(key, value)|
|
79
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
80
|
+
|
81
|
+
statement = [key.to_s.upcase]
|
82
|
+
statement << "BY" if %W(GROUP ORDER).include?(statement[0])
|
83
|
+
statement << to_segment(key, value)
|
84
|
+
statement.join(" ")
|
85
|
+
|
86
|
+
end.compact.join("\n").force_encoding("UTF-8")
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def generate_csv(rows, names = nil)
|
92
|
+
hashes = rows[0].is_a?(Hash)
|
93
|
+
|
94
|
+
if hashes
|
95
|
+
names ||= rows[0].keys
|
96
|
+
end
|
97
|
+
|
98
|
+
CSV.generate do |csv|
|
99
|
+
csv << names if names
|
100
|
+
rows.each do |row|
|
101
|
+
csv << (hashes ? row.values_at(*names) : row)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def inspect_value(value)
|
107
|
+
value.nil? ? "NULL" : value.inspect.gsub(/(^"|"$)/, "'").gsub("\\\"", "\"")
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_select_options(options)
|
111
|
+
keys = [:select, :from, :where, :group, :having, :order, :limit, :offset]
|
112
|
+
|
113
|
+
options = Hash[keys.zip(options.values_at(*keys))]
|
114
|
+
options[:select] ||= "*"
|
115
|
+
options[:limit] ||= 0 if options[:offset]
|
116
|
+
options[:limit] = options.values_at(:offset, :limit).compact.join(", ") if options[:limit]
|
117
|
+
options.delete(:offset)
|
118
|
+
|
119
|
+
options
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_segment(type, value)
|
123
|
+
case type
|
124
|
+
when :select
|
125
|
+
[value].flatten.join(", ")
|
126
|
+
when :where, :having
|
127
|
+
value.is_a?(Hash) ? to_condition_statements(value) : value
|
128
|
+
else
|
129
|
+
value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_condition_statements(value)
|
134
|
+
value.collect do |attr, val|
|
135
|
+
if val == :empty
|
136
|
+
"empty(#{attr})"
|
137
|
+
elsif val.is_a?(Range)
|
138
|
+
[
|
139
|
+
"#{attr} >= #{inspect_value(val.first)}",
|
140
|
+
"#{attr} <= #{inspect_value(val.last)}"
|
141
|
+
]
|
142
|
+
elsif val.is_a?(Array)
|
143
|
+
"#{attr} IN (#{val.collect{|x| inspect_value(x)}.join(", ")})"
|
144
|
+
elsif val.to_s.match(/^`.*`$/)
|
145
|
+
"#{attr} #{val.gsub(/(^`|`$)/, "")}"
|
146
|
+
else
|
147
|
+
"#{attr} = #{inspect_value(val)}"
|
148
|
+
end
|
149
|
+
end.flatten.join(" AND ")
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_data(data)
|
153
|
+
names = data["meta"].collect{|column| column["name"]}
|
154
|
+
types = data["meta"].collect{|column| column["type"]}
|
155
|
+
ResultSet.new data["data"], names, types
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
class Connection
|
3
|
+
module Query
|
4
|
+
class ResultRow < Array
|
5
|
+
|
6
|
+
def initialize(values = [], keys = nil)
|
7
|
+
super values
|
8
|
+
@keys = normalize_keys(keys)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash(symbolize = false)
|
12
|
+
@hash ||= begin
|
13
|
+
keys = symbolize ? @keys.collect(&:to_sym) : @keys
|
14
|
+
Hash[keys.zip(self)]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def normalize_keys(keys)
|
21
|
+
if keys
|
22
|
+
keys.collect do |key|
|
23
|
+
key.match(/^any\(([^\)]+)\)$/)
|
24
|
+
$1 || key
|
25
|
+
end
|
26
|
+
else
|
27
|
+
(0..(size - 1)).collect do |index|
|
28
|
+
"column#{index}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
class Connection
|
3
|
+
module Query
|
4
|
+
class ResultSet
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@rows, :size, :empty?
|
9
|
+
def_delegators :to_a, :first, :last, :flatten
|
10
|
+
|
11
|
+
attr_reader :names, :types
|
12
|
+
|
13
|
+
def initialize(rows = [], names = nil, types = nil)
|
14
|
+
@rows = rows
|
15
|
+
@names = names
|
16
|
+
@types = types
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
(0..(size - 1)).collect do |index|
|
21
|
+
yield self[index]
|
22
|
+
self[index]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](index)
|
27
|
+
row = @rows[index]
|
28
|
+
row = @rows[index] = parse_row(row) if row.class == Array
|
29
|
+
row
|
30
|
+
end
|
31
|
+
|
32
|
+
def present?
|
33
|
+
!empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_hashes(symbolize = false)
|
37
|
+
collect{|row| row.to_hash(symbolize)}
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse_row(array)
|
43
|
+
values = array.each_with_index.to_a.collect do |value, i|
|
44
|
+
parse_value(@types[i], value) if @types
|
45
|
+
end
|
46
|
+
ResultRow.new values, @names
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_value(type, value)
|
50
|
+
if value
|
51
|
+
case type
|
52
|
+
when "UInt8", "UInt16", "UInt32", "UInt64", "Int8", "Int16", "Int32", "Int64"
|
53
|
+
parse_int_value value
|
54
|
+
when "Float32", "Float64"
|
55
|
+
parse_float_value value
|
56
|
+
when "String", "Enum8", "Enum16"
|
57
|
+
parse_string_value value
|
58
|
+
when /FixedString\(\d+\)/
|
59
|
+
parse_fixed_string_value value
|
60
|
+
when "Date"
|
61
|
+
parse_date_value value
|
62
|
+
when "DateTime"
|
63
|
+
parse_date_time_value value
|
64
|
+
when /Array\(/
|
65
|
+
parse_array_value value
|
66
|
+
else
|
67
|
+
raise NotImplementedError, "Cannot parse value of type #{type.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_int_value(value)
|
73
|
+
value.to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_float_value(value)
|
77
|
+
value.to_f
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_string_value(value)
|
81
|
+
value.force_encoding("UTF-8")
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_fixed_string_value(value)
|
85
|
+
value.delete("\000").force_encoding("UTF-8")
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_date_value(value)
|
89
|
+
Date.parse(value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_date_time_value(value)
|
93
|
+
Time.parse(value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_array_value(value)
|
97
|
+
value
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
class Connection
|
3
|
+
module Query
|
4
|
+
class Table
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
@columns = []
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def engine(value)
|
13
|
+
@engine = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_sql
|
17
|
+
raise Clickhouse::InvalidQueryError, "Missing table engine" unless @engine
|
18
|
+
length = @columns.collect{|x| x[0].to_s.size}.max
|
19
|
+
|
20
|
+
sql = []
|
21
|
+
sql << "CREATE TABLE #{@name} ("
|
22
|
+
|
23
|
+
@columns.each_with_index do |(name, type), index|
|
24
|
+
sql << " #{name.ljust(length, " ")} #{type}#{"," unless index == @columns.size - 1}"
|
25
|
+
end
|
26
|
+
|
27
|
+
sql << ")"
|
28
|
+
sql << "ENGINE = #{@engine}"
|
29
|
+
|
30
|
+
sql.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def method_missing(name, *args)
|
36
|
+
type = name.to_s
|
37
|
+
.gsub(/(^.|_\w)/) {
|
38
|
+
$1.upcase
|
39
|
+
}
|
40
|
+
.gsub("Uint", "UInt")
|
41
|
+
.delete("_")
|
42
|
+
|
43
|
+
type << "(#{args[1]})" if args[1]
|
44
|
+
@columns << [args[0].to_s, type]
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class ConnectionError < Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidConnectionError < ConnectionError
|
10
|
+
end
|
11
|
+
|
12
|
+
class QueryError < Error
|
13
|
+
end
|
14
|
+
|
15
|
+
class InvalidQueryError < QueryError
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
module Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def normalize_url(url)
|
6
|
+
if url.match(/^\w+:\/\//)
|
7
|
+
url
|
8
|
+
else
|
9
|
+
"#{Clickhouse::Connection::DEFAULT_CONFIG[:scheme]}://#{url}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def extract_format(query)
|
14
|
+
format = nil
|
15
|
+
query = query.gsub(/ FORMAT (\w+)/i) do
|
16
|
+
format = $1
|
17
|
+
""
|
18
|
+
end
|
19
|
+
[query.strip, format]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "bundler"
|
5
|
+
Bundler.require :default, :development
|
6
|
+
|
7
|
+
def connect!(config = {})
|
8
|
+
Clickhouse.logger = Logger.new(STDOUT)
|
9
|
+
Clickhouse.establish_connection(config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def conn(config = {})
|
13
|
+
connect!(config) unless Clickhouse.connection
|
14
|
+
Clickhouse.connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def events
|
18
|
+
"events"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_table
|
22
|
+
conn.create_table(events) do |t|
|
23
|
+
t.fixed_string :id, 16
|
24
|
+
t.uint16 :year
|
25
|
+
t.date :date
|
26
|
+
t.date_time :time
|
27
|
+
t.string :event
|
28
|
+
t.uint32 :user_id
|
29
|
+
t.float32 :revenue
|
30
|
+
t.engine "MergeTree(date, (year, date), 8192)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def insert_rows
|
35
|
+
conn.insert_rows(events, :names => %w(id year date time event user_id revenue)) do |rows|
|
36
|
+
rows << [
|
37
|
+
"d91d1c90",
|
38
|
+
2016,
|
39
|
+
"2016-10-17",
|
40
|
+
"2016-10-17 23:14:28",
|
41
|
+
"click",
|
42
|
+
1982,
|
43
|
+
0.18
|
44
|
+
]
|
45
|
+
rows << [
|
46
|
+
"d91d2294",
|
47
|
+
2016,
|
48
|
+
"2016-10-17",
|
49
|
+
"2016-10-17 23:14:41",
|
50
|
+
"click",
|
51
|
+
1947,
|
52
|
+
0.203
|
53
|
+
]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
puts "Loading Clickhouse development environment (#{Clickhouse::VERSION})"
|
58
|
+
Pry.start
|