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,12 @@
1
+ module Clickhouse
2
+ class Connection
3
+ module Logger
4
+ private
5
+
6
+ def log(type, msg)
7
+ Clickhouse.logger.send(type, msg) if Clickhouse.logger
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Clickhouse
2
+ MAJOR = 0
3
+ MINOR = 0
4
+ TINY = 1
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join(".")
7
+ end
@@ -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