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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +125 -0
- data/.travis.yml +16 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +67 -0
- data/Makefile +9 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/click_house.gemspec +31 -0
- data/doc/logo.svg +37 -0
- data/docker-compose.yml +21 -0
- data/lib/click_house.rb +53 -0
- data/lib/click_house/config.rb +61 -0
- data/lib/click_house/connection.rb +48 -0
- data/lib/click_house/definition.rb +8 -0
- data/lib/click_house/definition/column.rb +46 -0
- data/lib/click_house/definition/column_set.rb +95 -0
- data/lib/click_house/errors.rb +7 -0
- data/lib/click_house/extend.rb +14 -0
- data/lib/click_house/extend/configurable.rb +11 -0
- data/lib/click_house/extend/connectible.rb +15 -0
- data/lib/click_house/extend/connection_database.rb +37 -0
- data/lib/click_house/extend/connection_healthy.rb +16 -0
- data/lib/click_house/extend/connection_inserting.rb +13 -0
- data/lib/click_house/extend/connection_selective.rb +23 -0
- data/lib/click_house/extend/connection_table.rb +81 -0
- data/lib/click_house/extend/type_definition.rb +15 -0
- data/lib/click_house/middleware.rb +9 -0
- data/lib/click_house/middleware/logging.rb +61 -0
- data/lib/click_house/middleware/parse_csv.rb +17 -0
- data/lib/click_house/middleware/raise_error.rb +25 -0
- data/lib/click_house/response.rb +8 -0
- data/lib/click_house/response/factory.rb +17 -0
- data/lib/click_house/response/result_set.rb +79 -0
- data/lib/click_house/type.rb +16 -0
- data/lib/click_house/type/base_type.rb +15 -0
- data/lib/click_house/type/boolean_type.rb +18 -0
- data/lib/click_house/type/date_time_type.rb +15 -0
- data/lib/click_house/type/date_type.rb +15 -0
- data/lib/click_house/type/decimal_type.rb +15 -0
- data/lib/click_house/type/fixed_string_type.rb +15 -0
- data/lib/click_house/type/float_type.rb +15 -0
- data/lib/click_house/type/integer_type.rb +15 -0
- data/lib/click_house/type/nullable_type.rb +21 -0
- data/lib/click_house/type/undefined_type.rb +15 -0
- data/lib/click_house/util.rb +8 -0
- data/lib/click_house/util/pretty.rb +30 -0
- data/lib/click_house/util/statement.rb +21 -0
- data/lib/click_house/version.rb +5 -0
- data/log/.keep +0 -0
- data/tmp/.keep +1 -0
- 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,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,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[1m[35mSQL (#{Util::Pretty.measure(elapsed)})\e[0m #{query};")
|
36
|
+
logger.debug(body) if body
|
37
|
+
logger.info("\e[1m[36mRead: #{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,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
|