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.
- 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
|