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