click_house 1.0.0

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/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +125 -0
  5. data/.travis.yml +16 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +67 -0
  8. data/Makefile +9 -0
  9. data/README.md +413 -0
  10. data/Rakefile +8 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +8 -0
  13. data/click_house.gemspec +31 -0
  14. data/doc/logo.svg +37 -0
  15. data/docker-compose.yml +21 -0
  16. data/lib/click_house.rb +53 -0
  17. data/lib/click_house/config.rb +61 -0
  18. data/lib/click_house/connection.rb +48 -0
  19. data/lib/click_house/definition.rb +8 -0
  20. data/lib/click_house/definition/column.rb +46 -0
  21. data/lib/click_house/definition/column_set.rb +95 -0
  22. data/lib/click_house/errors.rb +7 -0
  23. data/lib/click_house/extend.rb +14 -0
  24. data/lib/click_house/extend/configurable.rb +11 -0
  25. data/lib/click_house/extend/connectible.rb +15 -0
  26. data/lib/click_house/extend/connection_database.rb +37 -0
  27. data/lib/click_house/extend/connection_healthy.rb +16 -0
  28. data/lib/click_house/extend/connection_inserting.rb +13 -0
  29. data/lib/click_house/extend/connection_selective.rb +23 -0
  30. data/lib/click_house/extend/connection_table.rb +81 -0
  31. data/lib/click_house/extend/type_definition.rb +15 -0
  32. data/lib/click_house/middleware.rb +9 -0
  33. data/lib/click_house/middleware/logging.rb +61 -0
  34. data/lib/click_house/middleware/parse_csv.rb +17 -0
  35. data/lib/click_house/middleware/raise_error.rb +25 -0
  36. data/lib/click_house/response.rb +8 -0
  37. data/lib/click_house/response/factory.rb +17 -0
  38. data/lib/click_house/response/result_set.rb +79 -0
  39. data/lib/click_house/type.rb +16 -0
  40. data/lib/click_house/type/base_type.rb +15 -0
  41. data/lib/click_house/type/boolean_type.rb +18 -0
  42. data/lib/click_house/type/date_time_type.rb +15 -0
  43. data/lib/click_house/type/date_type.rb +15 -0
  44. data/lib/click_house/type/decimal_type.rb +15 -0
  45. data/lib/click_house/type/fixed_string_type.rb +15 -0
  46. data/lib/click_house/type/float_type.rb +15 -0
  47. data/lib/click_house/type/integer_type.rb +15 -0
  48. data/lib/click_house/type/nullable_type.rb +21 -0
  49. data/lib/click_house/type/undefined_type.rb +15 -0
  50. data/lib/click_house/util.rb +8 -0
  51. data/lib/click_house/util/pretty.rb +30 -0
  52. data/lib/click_house/util/statement.rb +21 -0
  53. data/lib/click_house/version.rb +5 -0
  54. data/log/.keep +0 -0
  55. data/tmp/.keep +1 -0
  56. metadata +208 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ autoload :TypeDefinition, 'click_house/extend/type_definition'
6
+ autoload :Configurable, 'click_house/extend/configurable'
7
+ autoload :Connectible, 'click_house/extend/connectible'
8
+ autoload :ConnectionHealthy, 'click_house/extend/connection_healthy'
9
+ autoload :ConnectionDatabase, 'click_house/extend/connection_database'
10
+ autoload :ConnectionTable, 'click_house/extend/connection_table'
11
+ autoload :ConnectionSelective, 'click_house/extend/connection_selective'
12
+ autoload :ConnectionInserting, 'click_house/extend/connection_inserting'
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module Configurable
6
+ def config(&block)
7
+ @config ||= Config.new(&block)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module Connectible
6
+ def connection=(connection)
7
+ @connection = connection
8
+ end
9
+
10
+ def connection
11
+ @connection ||= Connection.new(config.clone)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module ConnectionDatabase
6
+ # @return [Array<String>]
7
+ def databases
8
+ Array(execute('SHOW DATABASES FORMAT CSV', database: nil).body).tap(&:flatten!)
9
+ end
10
+
11
+ def create_database(name, if_not_exists: false, cluster: nil, engine: nil)
12
+ sql = 'CREATE DATABASE %<exists>s %<name>s %<cluster>s %<engine>s'
13
+
14
+ pattern = {
15
+ name: name,
16
+ exists: Util::Statement.ensure(if_not_exists, 'IF NOT EXISTS'),
17
+ cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"),
18
+ engine: Util::Statement.ensure(engine, "ENGINE = #{engine}")
19
+ }
20
+
21
+ execute(format(sql, pattern), database: nil).success?
22
+ end
23
+
24
+ def drop_database(name, if_exists: false, cluster: nil)
25
+ sql = 'DROP DATABASE %<exists>s %<name>s %<cluster>s'
26
+
27
+ pattern = {
28
+ name: name,
29
+ exists: Util::Statement.ensure(if_exists, 'IF EXISTS'),
30
+ cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"),
31
+ }
32
+
33
+ execute(format(sql, pattern), database: nil).success?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module ConnectionHealthy
6
+ def ping
7
+ # without +send_progress_in_http_headers: nil+ DB::Exception: Empty query returns
8
+ get(database: nil, query: { send_progress_in_http_headers: nil }).success?
9
+ end
10
+
11
+ def replicas_status
12
+ get('/replicas_status', database: nil).success?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module ConnectionInserting
6
+ def insert(table, columns:, values: [])
7
+ yield(values) if block_given?
8
+ body = "#{columns.to_csv}#{values.map(&:to_csv).join('')}"
9
+ execute("INSERT INTO #{table} FORMAT CSVWithNames", body).success?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module ConnectionSelective
6
+ # @return [ResultSet]
7
+ def select_all(sql)
8
+ response = execute(Util::Statement.format(sql, 'JSON'))
9
+ Response::Factory[response]
10
+ end
11
+
12
+ def select_value(sql)
13
+ response = execute(Util::Statement.format(sql, 'JSON'))
14
+ Array(Response::Factory[response].first).dig(0, -1)
15
+ end
16
+
17
+ def select_one(sql)
18
+ response = execute(Util::Statement.format(sql, 'JSON'))
19
+ Response::Factory[response].first
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module ConnectionTable
6
+ # @return [Array<String>]
7
+ def tables
8
+ Array(execute('SHOW TABLES FORMAT CSV').body).tap(&:flatten!)
9
+ end
10
+
11
+ # @return [ResultSet]
12
+ def describe_table(name)
13
+ Response::Factory[execute("DESCRIBE TABLE #{name} FORMAT JSON")]
14
+ end
15
+
16
+ # @return [Boolean]
17
+ def table_exists?(name, temporary: false)
18
+ sql = 'EXISTS %<temporary>s TABLE %<name>s FORMAT CSV'
19
+
20
+ pattern = {
21
+ name: name,
22
+ temporary: Util::Statement.ensure(temporary, 'TEMPORARY')
23
+ }
24
+
25
+ Type::BooleanType.new.cast(execute(format(sql, pattern)).body.dig(0, 0))
26
+ end
27
+
28
+ def drop_table(name, temporary: false, if_exists: false, cluster: nil)
29
+ sql = 'DROP %<temporary>s TABLE %<exists>s %<name>s %<cluster>s'
30
+
31
+ pattern = {
32
+ name: name,
33
+ temporary: Util::Statement.ensure(temporary, 'TEMPORARY'),
34
+ exists: Util::Statement.ensure(if_exists, 'IF EXISTS'),
35
+ cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"),
36
+ }
37
+
38
+ execute(format(sql, pattern)).success?
39
+ end
40
+
41
+ # rubocop:disable Metrics/ParameterLists
42
+ def create_table(
43
+ name,
44
+ if_not_exists: false, cluster: nil,
45
+ partition: nil, order: nil, primary_key: nil, sample: nil, ttl: nil, settings: nil,
46
+ engine:,
47
+ &block
48
+ )
49
+ sql = <<~SQL
50
+ CREATE TABLE %<exists>s %<name>s %<cluster>s %<definition>s %<engine>s
51
+ %<partition>s
52
+ %<order>s
53
+ %<primary_key>s
54
+ %<sample>s
55
+ %<ttl>s
56
+ %<settings>s
57
+ SQL
58
+ definition = ClickHouse::Definition::ColumnSet.new(&block)
59
+
60
+ pattern = {
61
+ name: name,
62
+ exists: Util::Statement.ensure(if_not_exists, 'IF NOT EXISTS'),
63
+ definition: definition.to_s,
64
+ cluster: Util::Statement.ensure(cluster, "ON CLUSTER #{cluster}"),
65
+ partition: Util::Statement.ensure(partition, "PARTITION BY #{partition}"),
66
+ order: Util::Statement.ensure(order, "ORDER BY #{order}"),
67
+ primary_key: Util::Statement.ensure(primary_key, "PRIMARY KEY #{primary_key}"),
68
+ sample: Util::Statement.ensure(sample, "SAMPLE BY #{sample}"),
69
+ ttl: Util::Statement.ensure(ttl, "TTL #{ttl}"),
70
+ settings: Util::Statement.ensure(settings, "SETTINGS #{settings}"),
71
+ engine: Util::Statement.ensure(engine, "ENGINE = #{engine}"),
72
+ }
73
+
74
+ puts format(sql, pattern)
75
+
76
+ execute(format(sql, pattern)).success?
77
+ end
78
+ # rubocop:enable Metrics/ParameterLists
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Extend
5
+ module TypeDefinition
6
+ def types
7
+ @types ||= Hash.new(Type::UndefinedType.new)
8
+ end
9
+
10
+ def add_type(type, klass)
11
+ types[type] = klass
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Middleware
5
+ autoload :Logging, 'click_house/middleware/logging'
6
+ autoload :ParseCsv, 'click_house/middleware/parse_csv'
7
+ autoload :RaiseError, 'click_house/middleware/raise_error'
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Middleware
5
+ class Logging < Faraday::Middleware
6
+ Faraday::Response.register_middleware self => self
7
+
8
+ SUMMARY_HEADER = 'x-clickhouse-summary'
9
+
10
+ attr_reader :logger, :starting, :body
11
+
12
+ def initialize(app = nil, logger:)
13
+ @logger = logger
14
+ super(app)
15
+ end
16
+
17
+ def call(environment)
18
+ @starting = timestamp
19
+ @body = environment.body if log_body?
20
+ @app.call(environment).on_complete(&method(:on_complete))
21
+ end
22
+
23
+ private
24
+
25
+ def log_body?
26
+ logger.level == Logger::DEBUG
27
+ end
28
+
29
+ # rubocop:disable Metrics/LineLength
30
+ def on_complete(env)
31
+ summary = extract_summary(env.response_headers)
32
+ elapsed = duration
33
+ query = CGI.parse(env.url.query.to_s).dig('query', 0) || '[NO QUERY]'
34
+
35
+ logger.info("\e[1mSQL (#{Util::Pretty.measure(elapsed)})\e[0m #{query};")
36
+ logger.debug(body) if body
37
+ logger.info("\e[1mRead: #{summary.fetch(:read_rows)} rows, #{summary.fetch(:read_bytes)}. Written: #{summary.fetch(:written_rows)}, rows #{summary.fetch(:written_bytes)}\e[0m")
38
+ end
39
+ # rubocop:enable Metrics/LineLength
40
+
41
+ def duration
42
+ timestamp - starting
43
+ end
44
+
45
+ def timestamp
46
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
+ end
48
+
49
+ def extract_summary(headers)
50
+ JSON.parse(headers.fetch('x-clickhouse-summary', '{}')).tap do |summary|
51
+ summary[:read_rows] = summary['read_rows']
52
+ summary[:read_bytes] = Util::Pretty.size(summary['read_bytes'].to_i)
53
+ summary[:written_rows] = summary['written_rows']
54
+ summary[:written_bytes] = Util::Pretty.size(summary['written_bytes'].to_i)
55
+ end
56
+ rescue JSON::ParserError
57
+ {}
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Middleware
5
+ class ParseCsv < FaradayMiddleware::ResponseMiddleware
6
+ Faraday::Response.register_middleware self => self
7
+
8
+ dependency do
9
+ require 'csv' unless defined?(CSV)
10
+ end
11
+
12
+ define_parser do |body, parser_options|
13
+ CSV.parse(body, parser_options || {}) unless body.strip.empty?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Middleware
5
+ class RaiseError < Faraday::Middleware
6
+ SUCCEED_STATUSES = (200..299).freeze
7
+
8
+ Faraday::Response.register_middleware self => self
9
+
10
+ def call(environment)
11
+ @app.call(environment).on_complete(&method(:on_complete))
12
+ rescue Faraday::ConnectionFailed => e
13
+ raise NetworkException, e.message, e.backtrace
14
+ end
15
+
16
+ private
17
+
18
+ def on_complete(env)
19
+ return if SUCCEED_STATUSES.include?(env.status)
20
+
21
+ raise DbException, "[#{env.status}] #{env.body}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Response
5
+ autoload :Factory, 'click_house/response/factory'
6
+ autoload :ResultSet, 'click_house/response/result_set'
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Response
5
+ class Factory
6
+ # @return [String, ResultSet]
7
+ # @params env [Faraday::Response]
8
+ def self.[](faraday)
9
+ body = faraday.body
10
+
11
+ return body if !body.is_a?(Hash) || !(body.key?('meta') && body.key?('data'))
12
+
13
+ ResultSet.new(meta: body.fetch('meta'), data: body.fetch('data'), statistics: body['statistics'])
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Response
5
+ class ResultSet
6
+ extend Forwardable
7
+ include Enumerable
8
+
9
+ TYPE_ARGV_DELIM = ','
10
+ NULLABLE = 'Nullable'
11
+
12
+ def_delegators :to_a, :each, :fetch
13
+
14
+ attr_reader :meta, :data, :statistics
15
+
16
+ class << self
17
+ # @return [Array<String, Array>]
18
+ # * first element is name of "ClickHouse.types.keys"
19
+ # * second element is extra arguments that should to be passed to <cast> function
20
+ #
21
+ # @input "DateTime('Europe/Moscow')"
22
+ # @output "DateTime(%s)"
23
+ #
24
+ # @input "Nullable(Decimal(10, 5))"
25
+ # @output "Nullable(Decimal(%s, %s))"
26
+ #
27
+ # @input "Decimal(10, 5)"
28
+ # @output "Decimal(%s, %s)"
29
+ def extract_type_info(type)
30
+ type = type.gsub(/#{NULLABLE}\((.+)\)/i, '\1')
31
+ nullable = Regexp.last_match(1)
32
+ argv = []
33
+
34
+ type = type.gsub(/\((.+)\)/, '')
35
+
36
+ if (match = Regexp.last_match(1))
37
+ counter = Array.new(match.count(TYPE_ARGV_DELIM).next) { '%s' }
38
+ type = "#{type}(#{counter.join("#{TYPE_ARGV_DELIM} ")})"
39
+ argv = match.split("#{TYPE_ARGV_DELIM} ")
40
+ end
41
+
42
+ [nullable ? "#{NULLABLE}(#{type})" : type, argv]
43
+ end
44
+ end
45
+
46
+ # @param meta [Array]
47
+ # @param data [Array]
48
+ def initialize(meta:, data:, statistics: nil)
49
+ @meta = meta
50
+ @data = data
51
+ @statistics = Hash(statistics)
52
+ end
53
+
54
+ def to_a
55
+ @to_a ||= data.each do |row|
56
+ row.each do |name, value|
57
+ casting = types.fetch(name)
58
+ row[name] = casting.fetch(:caster).cast(value, *casting.fetch(:arguments))
59
+ end
60
+ end
61
+ end
62
+
63
+ def types
64
+ @types ||= meta.each_with_object({}) do |row, object|
65
+ type_name, argv = self.class.extract_type_info(row.fetch('type'))
66
+
67
+ object[row.fetch('name')] = {
68
+ caster: ClickHouse.types[type_name],
69
+ arguments: argv
70
+ }
71
+ end
72
+ end
73
+
74
+ def inspect
75
+ to_a
76
+ end
77
+ end
78
+ end
79
+ end