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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'click_house'
5
+ require 'pry'
6
+
7
+ ClickHouse.config do |config|
8
+ config.logger = Logger.new(STDOUT)
9
+ end
10
+
11
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'click_house/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'click_house'
7
+ spec.version = ClickHouse::VERSION
8
+ spec.authors = ['Aliaksandr Shylau']
9
+ spec.email = ['alex.shilov.by@gmail.com']
10
+ spec.summary = 'Modern Ruby database driver for ClickHouse'
11
+ spec.description = 'Yandex ClickHouse database interface for Ruby'
12
+ spec.homepage = 'https://github.com/shlima/click_house'
13
+
14
+ # Specify which files should be added to the gem when it is released.
15
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'faraday'
24
+ spec.add_dependency 'faraday_middleware'
25
+ spec.add_development_dependency 'bundler', '~> 1.17'
26
+ spec.add_development_dependency 'rake', '~> 13.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'rubocop-performance'
31
+ end
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="150px" height="170px" viewBox="0 0 15 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
4
+ <title>Group</title>
5
+ <desc>Created with Sketch.</desc>
6
+ <defs>
7
+ <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
8
+ <stop stop-color="#FFE800" offset="0%"></stop>
9
+ <stop stop-color="#FFCC00" stop-opacity="0" offset="100%"></stop>
10
+ </linearGradient>
11
+ <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
12
+ <stop stop-color="#FFE800" stop-opacity="0.99" offset="0%"></stop>
13
+ <stop stop-color="#FFCC00" stop-opacity="0" offset="100%"></stop>
14
+ </linearGradient>
15
+ </defs>
16
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
17
+ <g id="Group">
18
+ <g transform="translate(3.000000, 5.000000)" id="Path">
19
+ <polygon fill="url(#linearGradient-1)" fill-rule="nonzero" points="0 0 2 0 2 12 0 12"></polygon>
20
+ <polygon fill="url(#linearGradient-2)" fill-rule="nonzero" points="3 0 5 0 5 12 3 12"></polygon>
21
+ <polygon fill="url(#linearGradient-1)" fill-rule="nonzero" points="6 0 8 0 8 12 6 12"></polygon>
22
+ </g>
23
+ <g id="68747470733a2f2f63646e2e7261776769742e636f6d2f636c61726976652f636c612d727562792d706c7567696e2f6d61737465722f7075626c69632f69636f6e2f727562792e7376673f73616e6974697a653d74727565">
24
+ <g id="Group-2" fill-rule="nonzero">
25
+ <polygon id="Path" fill="#CE0000" points="4.5 3.2 9.8 3.2 7.1 11.2"></polygon>
26
+ <polygon id="Path" fill="#7C0000" points="9.7 3.2 14.1 3.2 7.1 11.1"></polygon>
27
+ <polygon id="Path" fill="#F87274" points="4.5 3.2 0.1 3.2 7.1 11.1"></polygon>
28
+ <polygon id="Path" fill="#F87274" points="2.4 0.1 4.5 3.3 7.1 0.1"></polygon>
29
+ <polygon id="Path" fill="#FB191D" points="7.1 0.1 9.7 3.3 11.7 0.1"></polygon>
30
+ <polygon id="Path" fill="#CC0001" points="9.7 3.2 11.7 4.4408921e-16 14.2 3.2"></polygon>
31
+ <polygon id="Path" fill="#FB191D" points="-8.8817842e-16 3.2 2.4 4.4408921e-16 4.6 3.2"></polygon>
32
+ <polygon id="Path" fill="#900000" points="4.5 3.2 7.1 4.4408921e-16 9.8 3.2"></polygon>
33
+ </g>
34
+ </g>
35
+ </g>
36
+ </g>
37
+ </svg>
@@ -0,0 +1,21 @@
1
+ version: '3.5'
2
+
3
+ services:
4
+ clickhouse:
5
+ image: yandex/clickhouse-server
6
+ ports:
7
+ - "8123:8123"
8
+ - "9000:9000"
9
+ - "9009:9009"
10
+ ulimits:
11
+ nproc: 65535
12
+ nofile:
13
+ soft: 262144
14
+ hard: 262144
15
+ volumes:
16
+ - ./tmp/clickhouse-data:/opt/clickhouse/data
17
+ networks:
18
+ - default
19
+
20
+ networks:
21
+ default:
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'json'
5
+ require 'csv'
6
+ require 'uri'
7
+ require 'logger'
8
+ require 'faraday'
9
+ require 'forwardable'
10
+ require 'bigdecimal'
11
+ require 'faraday_middleware'
12
+ require 'click_house/version'
13
+ require 'click_house/errors'
14
+ require 'click_house/response'
15
+ require 'click_house/type'
16
+ require 'click_house/middleware'
17
+ require 'click_house/extend'
18
+ require 'click_house/util'
19
+ require 'click_house/definition'
20
+
21
+ module ClickHouse
22
+ extend Extend::TypeDefinition
23
+ extend Extend::Configurable
24
+ extend Extend::Connectible
25
+
26
+ autoload :Config, 'click_house/config'
27
+ autoload :Connection, 'click_house/connection'
28
+
29
+ %w[Date].each do |column|
30
+ add_type column, Type::DateType.new
31
+ add_type "Nullable(#{column})", Type::NullableType.new(Type::DateType.new)
32
+ end
33
+
34
+ ['DateTime(%s)'].each do |column|
35
+ add_type column, Type::DateTimeType.new
36
+ add_type "Nullable(#{column})", Type::NullableType.new(Type::DateTimeType.new)
37
+ end
38
+
39
+ ['Decimal(%s, %s)', 'Decimal32(%s)', 'Decimal64(%s)', 'Decimal128(%s)'].each do |column|
40
+ add_type column, Type::DecimalType.new
41
+ add_type "Nullable(#{column})", Type::NullableType.new(Type::DecimalType.new)
42
+ end
43
+
44
+ %w[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64].each do |column|
45
+ add_type column, Type::IntegerType.new
46
+ add_type "Nullable(#{column})", Type::NullableType.new(Type::IntegerType.new)
47
+ end
48
+
49
+ %w[Float32 Float64].each do |column|
50
+ add_type column, Type::FloatType.new
51
+ add_type "Nullable(#{column})", Type::NullableType.new(Type::IntegerType.new)
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ class Config
5
+ DEFAULT_SCHEME = 'http'
6
+ DEFAULT_HOST = 'localhost'
7
+ DEFAULT_PORT = '8123'
8
+
9
+ DEFAULTS = {
10
+ adapter: Faraday.default_adapter,
11
+ url: nil,
12
+ scheme: 'http',
13
+ host: 'localhost',
14
+ port: '8123',
15
+ logger: nil,
16
+ database: nil,
17
+ username: nil,
18
+ password: nil,
19
+ timeout: nil,
20
+ open_timeout: nil,
21
+ ssl_verify: false
22
+ }.freeze
23
+
24
+ attr_accessor :adapter
25
+ attr_accessor :logger
26
+ attr_accessor :scheme
27
+ attr_accessor :host
28
+ attr_accessor :port
29
+ attr_accessor :database
30
+ attr_accessor :url
31
+ attr_accessor :username
32
+ attr_accessor :password
33
+ attr_accessor :timeout
34
+ attr_accessor :open_timeout
35
+ attr_accessor :ssl_verify
36
+
37
+ def initialize(params = {})
38
+ assign(DEFAULTS.merge(params))
39
+ yield(self) if block_given?
40
+ end
41
+
42
+ # @return [self]
43
+ def assign(params = {})
44
+ params.each { |k, v| public_send("#{k}=", v) }
45
+
46
+ self
47
+ end
48
+
49
+ def auth?
50
+ !username.nil? || !password.nil?
51
+ end
52
+
53
+ def logger!
54
+ @logger || Logger.new('/dev/null')
55
+ end
56
+
57
+ def url!
58
+ @url || "#{scheme}://#{host}:#{port}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ class Connection
5
+ include Extend::ConnectionHealthy
6
+ include Extend::ConnectionDatabase
7
+ include Extend::ConnectionTable
8
+ include Extend::ConnectionSelective
9
+ include Extend::ConnectionInserting
10
+
11
+ attr_reader :config
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ end
16
+
17
+ def execute(query, body = nil, database: config.database)
18
+ post(body, query: { query: query }, database: database)
19
+ end
20
+
21
+ def get(path = '/', query: {}, database: config.database)
22
+ transport.get(compose(path, query.merge(database: database)))
23
+ end
24
+
25
+ def post(body = nil, query: {}, database: config.database)
26
+ transport.post(compose('/', query.merge(database: database)), body)
27
+ end
28
+
29
+ def transport
30
+ @transport ||= Faraday.new(config.url!) do |conn|
31
+ conn.options.timeout = config.timeout
32
+ conn.options.open_timeout = config.open_timeout
33
+ conn.ssl.verify = config.ssl_verify
34
+ conn.basic_auth(config.username, config.password) if config.auth?
35
+ conn.response Middleware::Logging, logger: config.logger!
36
+ conn.response Middleware::RaiseError
37
+ conn.response :json, content_type: %r{application/json}
38
+ conn.response Middleware::ParseCsv, content_type: %r{text/csv}
39
+ conn.adapter config.adapter
40
+ end
41
+ end
42
+
43
+ def compose(path, query = {})
44
+ # without <query.compact> "DB::Exception: Empty query" error will occur
45
+ "#{path}?#{URI.encode_www_form({ send_progress_in_http_headers: 1 }.merge(query).compact)}"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Definition
5
+ autoload :Column, 'click_house/definition/column'
6
+ autoload :ColumnSet, 'click_house/definition/column_set'
7
+ end
8
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Definition
5
+ class Column
6
+ attr_accessor :name
7
+ attr_accessor :type
8
+ attr_accessor :nullable
9
+ attr_accessor :extensions
10
+ attr_accessor :default
11
+ attr_accessor :materialized
12
+ attr_accessor :ttl
13
+
14
+ def initialize(params = {})
15
+ params.each { |k, v| public_send("#{k}=", v) }
16
+ yield(self) if block_given?
17
+ end
18
+
19
+ def to_s
20
+ nullable ? "#{name} Nullable(#{extension_type}) #{opts}" : "#{name} #{extension_type} #{opts}"
21
+ end
22
+
23
+ private
24
+
25
+ def opts
26
+ options = {
27
+ DEFAULT: Util::Statement.ensure(default, default),
28
+ MATERIALIZED: Util::Statement.ensure(materialized, materialized),
29
+ TTL: Util::Statement.ensure(ttl, ttl)
30
+ }.compact
31
+
32
+ result = options.each_with_object([]) do |(key, value), object|
33
+ object << "#{key} #{value}"
34
+ end
35
+
36
+ result.join(' ')
37
+ end
38
+
39
+ def extension_type
40
+ extensions.nil? ? type : format(type, *extensions)
41
+ rescue TypeError, ArgumentError
42
+ raise Exception, "please provide extensions for <#{type}>"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module Definition
5
+ class ColumnSet
6
+ TYPES = [
7
+ 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Int8', 'Int16', 'Int32', 'Int64',
8
+ 'Float32', 'Float64',
9
+ 'Decimal(%d, %d)', 'Decimal32(%d)', 'Decimal64(%d)', 'Decimal128(%d)',
10
+ 'String',
11
+ 'FixedString(%d)',
12
+ 'UUID',
13
+ 'Date',
14
+ "DateTime('%s')"
15
+ ].freeze
16
+
17
+ class << self
18
+ # @input "DateTime('%s')"
19
+ # @output "DateTime"
20
+ def method_name_for_type(type)
21
+ type.sub(/\(.+/, '')
22
+ end
23
+ end
24
+
25
+ TYPES.each do |type|
26
+ method_name = method_name_for_type(type)
27
+
28
+ # t.Decimal :customer_id, nullable: true, default: ''
29
+ # t.Decimal :money, 1, 2, nullable: true, default: ''
30
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
31
+ def #{method_name}(*definition)
32
+ name = definition[0]
33
+ extentions = []
34
+ options = {}
35
+ extensions = Array(definition[1..-1]).each do |el|
36
+ el.is_a?(Hash) ? options.merge!(el) : extentions.push(el)
37
+ end
38
+
39
+ columns << Column.new(type: "#{type}", name: name, extensions: extensions, **options)
40
+ end
41
+ METHODS
42
+ end
43
+
44
+ def initialize
45
+ yield(self) if block_given?
46
+ end
47
+
48
+ def columns
49
+ @columns ||= []
50
+ end
51
+
52
+ def to_s
53
+ <<~SQL
54
+ ( #{columns.map(&:to_s).join(', ')} )
55
+ SQL
56
+ end
57
+
58
+ # @example
59
+ # t.Nested :json do |n|
60
+ # n.UInt8 :city_id
61
+ # end
62
+ def nested(name, &block)
63
+ columns << "#{name} Nested #{ColumnSet.new(&block)}"
64
+ end
65
+
66
+ alias_method :Nested, :nested
67
+
68
+ def push(sql)
69
+ columns << sql
70
+ end
71
+
72
+ alias_method :<<, :push
73
+ end
74
+ end
75
+ end
76
+
77
+ __END__
78
+
79
+ data = ClickHouse::Definition::ColumnSet.new do |t|
80
+ t << "words Enum('hello' = 1, 'world' = 2)"
81
+ end
82
+
83
+ puts data.to_s
84
+
85
+ data = ClickHouse::Definition::ColumnSet.new do |t|
86
+ t.Decimal :money
87
+ t.Float32 :client_id, default: 0
88
+ t.Float32 :city_id, default: 0, nullable: true
89
+ t.Nested :json do |n|
90
+ n.Date :created_at
91
+ n.Date :updated_at
92
+ end
93
+
94
+ t << "CUSTOM SQL"
95
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ Error = Class.new(StandardError)
5
+ NetworkException = Class.new(Error)
6
+ DbException = Class.new(Error)
7
+ end