click_house 1.6.2 → 2.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 +4 -4
- data/.github/workflows/main.yml +16 -5
- data/.rubocop.yml +77 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +41 -48
- data/Gemfile_faraday1 +11 -0
- data/Gemfile_faraday1.lock +111 -0
- data/Gemfile_faraday2 +1 -0
- data/Gemfile_faraday2.lock +88 -0
- data/Makefile +25 -0
- data/README.md +6 -20
- data/click_house.gemspec +4 -3
- data/docker-compose.yml +1 -1
- data/lib/click_house/ast/parser.rb +53 -0
- data/lib/click_house/ast/statement.rb +99 -0
- data/lib/click_house/ast/ticker.rb +42 -0
- data/lib/click_house/ast.rb +9 -0
- data/lib/click_house/config.rb +17 -1
- data/lib/click_house/connection.rb +3 -2
- data/lib/click_house/definition/column.rb +6 -3
- data/lib/click_house/definition/column_set.rb +3 -2
- data/lib/click_house/extend/configurable.rb +2 -0
- data/lib/click_house/extend/connection_inserting.rb +3 -3
- data/lib/click_house/extend/type_definition.rb +1 -10
- data/lib/click_house/middleware/logging.rb +1 -1
- data/lib/click_house/middleware/parse_csv.rb +5 -6
- data/lib/click_house/middleware/parse_json.rb +16 -0
- data/lib/click_house/middleware/parse_json_oj.rb +22 -0
- data/lib/click_house/middleware/raise_error.rb +8 -8
- data/lib/click_house/middleware/response_base.rb +45 -0
- data/lib/click_house/middleware.rb +3 -0
- data/lib/click_house/response/result_set.rb +58 -40
- data/lib/click_house/response.rb +1 -0
- data/lib/click_house/type/array_type.rb +6 -12
- data/lib/click_house/type/base_type.rb +25 -1
- data/lib/click_house/type/date_time64_type.rb +5 -1
- data/lib/click_house/type/date_time_type.rb +5 -1
- data/lib/click_house/type/decimal_type.rb +15 -1
- data/lib/click_house/type/low_cardinality_type.rb +15 -0
- data/lib/click_house/type/map_type.rb +15 -0
- data/lib/click_house/type/nullable_type.rb +6 -8
- data/lib/click_house/type/string_type.rb +1 -1
- data/lib/click_house/type/tuple_type.rb +15 -0
- data/lib/click_house/type.rb +3 -0
- data/lib/click_house/version.rb +1 -1
- data/lib/click_house.rb +10 -3
- metadata +38 -4
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module ClickHouse
|
6
|
+
module Ast
|
7
|
+
class Parser
|
8
|
+
OPEN = '('
|
9
|
+
CLOSED = ')'
|
10
|
+
COMMA = ','
|
11
|
+
SPACE = ' '
|
12
|
+
|
13
|
+
attr_reader :input
|
14
|
+
|
15
|
+
# @param input [String]
|
16
|
+
def initialize(input)
|
17
|
+
@input = input
|
18
|
+
end
|
19
|
+
|
20
|
+
# @refs https://clickhouse.com/docs/en/sql-reference/data-types/
|
21
|
+
# Map(String, Decimal(10, 5))
|
22
|
+
# Array(Array(Array(Array(Nullable(Int, String)))))
|
23
|
+
def parse
|
24
|
+
ticker = Ticker.new
|
25
|
+
control = false
|
26
|
+
|
27
|
+
input.each_char do |char|
|
28
|
+
# cases like (1,<space after comma> 3)
|
29
|
+
next if control && char == SPACE
|
30
|
+
|
31
|
+
case char
|
32
|
+
when OPEN
|
33
|
+
control = true
|
34
|
+
ticker.open
|
35
|
+
when CLOSED
|
36
|
+
control = true
|
37
|
+
ticker.close
|
38
|
+
when COMMA
|
39
|
+
control = true
|
40
|
+
ticker.comma
|
41
|
+
else
|
42
|
+
control = false
|
43
|
+
ticker.char(char)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# if a single type like "Int"
|
48
|
+
ticker.current.name! unless control
|
49
|
+
ticker.current
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module ClickHouse
|
6
|
+
module Ast
|
7
|
+
class Statement
|
8
|
+
PLACEHOLDER_S = '%s'
|
9
|
+
PLACEHOLDER_D = '%d'
|
10
|
+
DIGIT_RE = /\A\d+\Z/.freeze
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
attr_accessor :caster
|
14
|
+
|
15
|
+
def initialize(name: '')
|
16
|
+
@buffer = ''
|
17
|
+
@name = name
|
18
|
+
@opened = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param value [String]
|
22
|
+
def print(value)
|
23
|
+
@buffer = "#{@buffer}#{value}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def name!
|
27
|
+
@name = @buffer
|
28
|
+
@buffer = ''
|
29
|
+
end
|
30
|
+
|
31
|
+
def argument!
|
32
|
+
add_argument(Statement.new(name: @buffer))
|
33
|
+
@buffer = ''
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param st [Statement]
|
37
|
+
def add_argument(st)
|
38
|
+
arguments.push(st)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param other [Statement]
|
42
|
+
def merge(other)
|
43
|
+
if other.named?
|
44
|
+
add_argument(other)
|
45
|
+
else
|
46
|
+
@arguments = arguments.concat(other.arguments)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def named?
|
51
|
+
!@name.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
def buffer?
|
55
|
+
!@buffer.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Array<Statement>]
|
59
|
+
def arguments
|
60
|
+
@arguments ||= []
|
61
|
+
end
|
62
|
+
|
63
|
+
def placeholder
|
64
|
+
return @placeholder if defined?(@placeholder)
|
65
|
+
|
66
|
+
@placeholder = digit? ? PLACEHOLDER_D : PLACEHOLDER_S
|
67
|
+
end
|
68
|
+
|
69
|
+
def digit?
|
70
|
+
name.match?(DIGIT_RE)
|
71
|
+
end
|
72
|
+
|
73
|
+
def value
|
74
|
+
@value ||=
|
75
|
+
case placeholder
|
76
|
+
when PLACEHOLDER_D
|
77
|
+
Integer(name)
|
78
|
+
when PLACEHOLDER_S
|
79
|
+
# remove leading and trailing quotes
|
80
|
+
name[1..-2]
|
81
|
+
else
|
82
|
+
raise "unknown value extractor for <#{placeholder}>"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
out = StringIO.new
|
88
|
+
out.print(name.empty? ? 'NO_NAME' : name)
|
89
|
+
out.print("<#{@buffer}>") unless @buffer.empty?
|
90
|
+
|
91
|
+
if arguments.any?
|
92
|
+
out.print("(#{arguments.join(',')})")
|
93
|
+
end
|
94
|
+
|
95
|
+
out.string
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module ClickHouse
|
6
|
+
module Ast
|
7
|
+
class Ticker
|
8
|
+
attr_reader :root, :current
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@current = Statement.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def open
|
15
|
+
current.name!
|
16
|
+
opened.push(current)
|
17
|
+
@current = Statement.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def comma
|
21
|
+
current.argument! if current.buffer?
|
22
|
+
opened.last.merge(current)
|
23
|
+
@current = Statement.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
current.argument! unless current.named?
|
28
|
+
opened.last.merge(current)
|
29
|
+
@current = opened.pop
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param char [String]
|
33
|
+
def char(char)
|
34
|
+
current.print(char)
|
35
|
+
end
|
36
|
+
|
37
|
+
def opened
|
38
|
+
@opened ||= []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/click_house/config.rb
CHANGED
@@ -16,7 +16,20 @@ module ClickHouse
|
|
16
16
|
open_timeout: nil,
|
17
17
|
ssl_verify: false,
|
18
18
|
headers: {},
|
19
|
-
global_params: {}
|
19
|
+
global_params: {},
|
20
|
+
json_parser: ClickHouse::Middleware::ParseJson,
|
21
|
+
oj_load_options: {
|
22
|
+
mode: :custom,
|
23
|
+
allow_blank: true,
|
24
|
+
bigdecimal_as_decimal: false, # dump BigDecimal as a String
|
25
|
+
bigdecimal_load: :bigdecimal, # convert all decimal numbers to BigDecimal
|
26
|
+
empty_string: false,
|
27
|
+
second_precision: 6,
|
28
|
+
time_format: :ruby,
|
29
|
+
},
|
30
|
+
json_load_options: {
|
31
|
+
decimal_class: BigDecimal,
|
32
|
+
},
|
20
33
|
}.freeze
|
21
34
|
|
22
35
|
attr_accessor :adapter
|
@@ -33,6 +46,9 @@ module ClickHouse
|
|
33
46
|
attr_accessor :ssl_verify
|
34
47
|
attr_accessor :headers
|
35
48
|
attr_accessor :global_params
|
49
|
+
attr_accessor :oj_load_options
|
50
|
+
attr_accessor :json_load_options
|
51
|
+
attr_accessor :json_parser # response middleware
|
36
52
|
|
37
53
|
def initialize(params = {})
|
38
54
|
assign(DEFAULTS.merge(params))
|
@@ -46,6 +46,7 @@ module ClickHouse
|
|
46
46
|
transport.post(compose('/', query.merge(database: database, **params)), body)
|
47
47
|
end
|
48
48
|
|
49
|
+
# transport should work the same both with Faraday v1 and Faraday v2
|
49
50
|
def transport
|
50
51
|
@transport ||= Faraday.new(config.url!) do |conn|
|
51
52
|
conn.options.timeout = config.timeout
|
@@ -55,8 +56,8 @@ module ClickHouse
|
|
55
56
|
conn.request(:basic_auth, config.username, config.password) if config.auth?
|
56
57
|
conn.response Middleware::RaiseError
|
57
58
|
conn.response Middleware::Logging, logger: config.logger!
|
58
|
-
conn.response
|
59
|
-
conn.response Middleware::ParseCsv, content_type: %r{text/csv}
|
59
|
+
conn.response config.json_parser, content_type: %r{application/json}, parser_options: { config: config }
|
60
|
+
conn.response Middleware::ParseCsv, content_type: %r{text/csv}, parser_options: { config: config }
|
60
61
|
conn.adapter config.adapter
|
61
62
|
end
|
62
63
|
end
|
@@ -6,6 +6,7 @@ module ClickHouse
|
|
6
6
|
attr_accessor :name
|
7
7
|
attr_accessor :type
|
8
8
|
attr_accessor :nullable
|
9
|
+
attr_accessor :low_cardinality
|
9
10
|
attr_accessor :extensions
|
10
11
|
attr_accessor :default
|
11
12
|
attr_accessor :materialized
|
@@ -17,10 +18,12 @@ module ClickHouse
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def to_s
|
20
|
-
|
21
|
-
|
21
|
+
type = extension_type
|
22
|
+
type = "Nullable(#{type})" if nullable
|
23
|
+
type = "LowCardinality(#{type})" if low_cardinality
|
22
24
|
|
23
|
-
|
25
|
+
"#{name} #{type} #{opts}"
|
26
|
+
end
|
24
27
|
|
25
28
|
def opts
|
26
29
|
options = {
|
@@ -3,7 +3,9 @@
|
|
3
3
|
module ClickHouse
|
4
4
|
module Definition
|
5
5
|
class ColumnSet
|
6
|
-
TYPES = ClickHouse.
|
6
|
+
TYPES = ClickHouse.types.each_with_object([]) do |(name, type), object|
|
7
|
+
object << name.sub('%s', "'%s'") if type.ddl?
|
8
|
+
end
|
7
9
|
|
8
10
|
class << self
|
9
11
|
# @input "DateTime('%s')"
|
@@ -15,7 +17,6 @@ module ClickHouse
|
|
15
17
|
|
16
18
|
TYPES.each do |type|
|
17
19
|
method_name = method_name_for_type(type)
|
18
|
-
|
19
20
|
# t.Decimal :customer_id, nullable: true, default: ''
|
20
21
|
# t.Decimal :money, 1, 2, nullable: true, default: ''
|
21
22
|
class_eval <<-METHODS, __FILE__, __LINE__ + 1
|
@@ -14,8 +14,8 @@ module ClickHouse
|
|
14
14
|
# end
|
15
15
|
#
|
16
16
|
# == Example with a param
|
17
|
-
# subject.insert('rspec', values: [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }])
|
18
|
-
def insert(table, columns: [], values: [])
|
17
|
+
# subject.insert('rspec', values: [{ name: 'Sun', id: 1 }, { name: 'Moon', id: 2 }], format: 'JSONStringsEachRow')
|
18
|
+
def insert(table, columns: [], values: [], format: 'JSONEachRow')
|
19
19
|
yield(values) if block_given?
|
20
20
|
|
21
21
|
body = if columns.empty?
|
@@ -26,7 +26,7 @@ module ClickHouse
|
|
26
26
|
|
27
27
|
return EMPTY_INSERT if values.empty?
|
28
28
|
|
29
|
-
execute("INSERT INTO #{table} FORMAT
|
29
|
+
execute("INSERT INTO #{table} FORMAT #{format}", body.join("\n")).success?
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
@@ -3,21 +3,12 @@
|
|
3
3
|
module ClickHouse
|
4
4
|
module Extend
|
5
5
|
module TypeDefinition
|
6
|
-
NULLABLE = 'Nullable'
|
7
|
-
NULLABLE_RE = /#{NULLABLE}/i.freeze
|
8
|
-
|
9
6
|
def types
|
10
7
|
@types ||= Hash.new(Type::UndefinedType.new)
|
11
8
|
end
|
12
9
|
|
13
|
-
def add_type(type, klass
|
10
|
+
def add_type(type, klass)
|
14
11
|
types[type] = klass
|
15
|
-
types["#{NULLABLE}(#{type})"] = Type::NullableType.new(klass) if nullable
|
16
|
-
end
|
17
|
-
|
18
|
-
# @return [Enum<String>]
|
19
|
-
def type_names(nullable:)
|
20
|
-
nullable ? types.keys : types.keys.grep_v(NULLABLE_RE)
|
21
12
|
end
|
22
13
|
end
|
23
14
|
end
|
@@ -30,7 +30,7 @@ module ClickHouse
|
|
30
30
|
def on_complete(env)
|
31
31
|
summary = extract_summary(env.response_headers)
|
32
32
|
logger.info("\e[1m[35mSQL (#{duration_stats_log(env.body)})\e[0m #{query(env)};")
|
33
|
-
logger.debug(body) if body
|
33
|
+
logger.debug(body) if body
|
34
34
|
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")
|
35
35
|
end
|
36
36
|
# rubocop:enable Layout/LineLength
|
@@ -2,15 +2,14 @@
|
|
2
2
|
|
3
3
|
module ClickHouse
|
4
4
|
module Middleware
|
5
|
-
class ParseCsv <
|
5
|
+
class ParseCsv < ResponseBase
|
6
6
|
Faraday::Response.register_middleware self => self
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
# @param env [Faraday::Env]
|
9
|
+
def on_complete(env)
|
10
|
+
return unless content_type?(env, content_type)
|
11
11
|
|
12
|
-
|
13
|
-
CSV.parse(body, **Hash.new(parser_options)) unless body.strip.empty?
|
12
|
+
env.body = env.body.strip.empty? ? nil : CSV.parse(env.body)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Middleware
|
5
|
+
class ParseJson < ResponseBase
|
6
|
+
Faraday::Response.register_middleware self => self
|
7
|
+
|
8
|
+
# @param env [Faraday::Env]
|
9
|
+
def on_complete(env)
|
10
|
+
return unless content_type?(env, content_type)
|
11
|
+
|
12
|
+
env.body = JSON.parse(env.body, config.json_load_options) unless env.body.strip.empty?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Middleware
|
5
|
+
class ParseJsonOj < ResponseBase
|
6
|
+
Faraday::Response.register_middleware self => self
|
7
|
+
|
8
|
+
# @param env [Faraday::Env]
|
9
|
+
def on_complete(env)
|
10
|
+
return unless content_type?(env, content_type)
|
11
|
+
|
12
|
+
env.body = Oj.load(env.body, config.oj_load_options) unless env.body.strip.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def on_setup
|
18
|
+
require 'oj' unless defined?(Oj)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -3,22 +3,22 @@
|
|
3
3
|
module ClickHouse
|
4
4
|
module Middleware
|
5
5
|
class RaiseError < Faraday::Middleware
|
6
|
-
|
6
|
+
EXCEPTION_CODE_HEADER = 'x-clickhouse-exception-code'
|
7
7
|
|
8
8
|
Faraday::Response.register_middleware self => self
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
# @param env [Faraday::Env]
|
11
|
+
def call(env)
|
12
|
+
super
|
12
13
|
rescue Faraday::ConnectionFailed => e
|
13
14
|
raise NetworkException, e.message, e.backtrace
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
# @param env [Faraday::Env]
|
18
18
|
def on_complete(env)
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
if env.response_headers.include?(EXCEPTION_CODE_HEADER) || !env.success?
|
20
|
+
raise DbException, "[#{env.status}] #{env.body}"
|
21
|
+
end
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Middleware
|
5
|
+
class ResponseBase < Faraday::Middleware
|
6
|
+
CONTENT_TYPE_HEADER = 'content-type'
|
7
|
+
|
8
|
+
attr_reader :parser_options
|
9
|
+
attr_reader :content_type
|
10
|
+
|
11
|
+
def initialize(app = nil, parser_options: {}, content_type: nil, preserve_raw: false)
|
12
|
+
super(app)
|
13
|
+
@parser_options = parser_options
|
14
|
+
@content_type = content_type
|
15
|
+
@preserve_raw = preserve_raw
|
16
|
+
on_setup
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Boolean]
|
20
|
+
# @param env [Faraday::Env]
|
21
|
+
# @param regexp [NilClass, Regexp]
|
22
|
+
def content_type?(env, regexp)
|
23
|
+
case regexp
|
24
|
+
when NilClass
|
25
|
+
false
|
26
|
+
when Regexp
|
27
|
+
regexp.match?(String(env[:response_headers][CONTENT_TYPE_HEADER]))
|
28
|
+
else
|
29
|
+
raise ArgumentError, "expected regexp got #{regexp.class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Config]
|
34
|
+
def config
|
35
|
+
parser_options.fetch(:config)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def on_setup
|
41
|
+
# require external dependencies here
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
module ClickHouse
|
4
4
|
module Middleware
|
5
|
+
autoload :ResponseBase, 'click_house/middleware/response_base'
|
5
6
|
autoload :Logging, 'click_house/middleware/logging'
|
6
7
|
autoload :ParseCsv, 'click_house/middleware/parse_csv'
|
8
|
+
autoload :ParseJsonOj, 'click_house/middleware/parse_json_oj'
|
9
|
+
autoload :ParseJson, 'click_house/middleware/parse_json'
|
7
10
|
autoload :RaiseError, 'click_house/middleware/raise_error'
|
8
11
|
end
|
9
12
|
end
|
@@ -6,9 +6,8 @@ module ClickHouse
|
|
6
6
|
extend Forwardable
|
7
7
|
include Enumerable
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
NULLABLE_TYPE_RE = /#{NULLABLE}\((.+)\)/i.freeze
|
9
|
+
PLACEHOLDER_D = '%d'
|
10
|
+
PLACEHOLDER_S = '%s'
|
12
11
|
|
13
12
|
def_delegators :to_a,
|
14
13
|
:inspect, :each, :fetch, :length, :count, :size,
|
@@ -16,36 +15,6 @@ module ClickHouse
|
|
16
15
|
|
17
16
|
attr_reader :meta, :data, :totals, :statistics, :rows_before_limit_at_least
|
18
17
|
|
19
|
-
class << self
|
20
|
-
# @return [Array<String, Array>]
|
21
|
-
# * first element is name of "ClickHouse.types.keys"
|
22
|
-
# * second element is extra arguments that should to be passed to <cast> function
|
23
|
-
#
|
24
|
-
# @input "DateTime('Europe/Moscow')"
|
25
|
-
# @output "DateTime(%s)"
|
26
|
-
#
|
27
|
-
# @input "Nullable(Decimal(10, 5))"
|
28
|
-
# @output "Nullable(Decimal(%s, %s))"
|
29
|
-
#
|
30
|
-
# @input "Decimal(10, 5)"
|
31
|
-
# @output "Decimal(%s, %s)"
|
32
|
-
def extract_type_info(type)
|
33
|
-
type = type.gsub(NULLABLE_TYPE_RE, '\1')
|
34
|
-
nullable = Regexp.last_match(1)
|
35
|
-
argv = []
|
36
|
-
|
37
|
-
type = type.gsub(/\((.+)\)/, '')
|
38
|
-
|
39
|
-
if (match = Regexp.last_match(1))
|
40
|
-
counter = Array.new(match.count(TYPE_ARGV_DELIM).next) { '%s' }
|
41
|
-
type = "#{type}(#{counter.join("#{TYPE_ARGV_DELIM} ")})"
|
42
|
-
argv = match.split("#{TYPE_ARGV_DELIM} ")
|
43
|
-
end
|
44
|
-
|
45
|
-
[nullable ? "#{NULLABLE}(#{type})" : type, argv]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
18
|
# @param meta [Array]
|
50
19
|
# @param data [Array]
|
51
20
|
# @param totals [Array|Hash|NilClass] Support for 'GROUP BY WITH TOTALS' modifier
|
@@ -62,20 +31,69 @@ module ClickHouse
|
|
62
31
|
def to_a
|
63
32
|
@to_a ||= data.each do |row|
|
64
33
|
row.each do |name, value|
|
65
|
-
|
66
|
-
row[name] = casting.fetch(:caster).cast(value, *casting.fetch(:arguments))
|
34
|
+
row[name] = cast_type(types.fetch(name), value)
|
67
35
|
end
|
68
36
|
end
|
69
37
|
end
|
70
38
|
|
39
|
+
# @return [Hash<String, Ast::Statement>]
|
71
40
|
def types
|
72
41
|
@types ||= meta.each_with_object({}) do |row, object|
|
73
|
-
|
42
|
+
object[row.fetch('name')] = begin
|
43
|
+
current = Ast::Parser.new(row.fetch('type')).parse
|
44
|
+
assign_type(current)
|
45
|
+
current
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @param stmt [Ast::Statement]
|
53
|
+
def assign_type(stmt)
|
54
|
+
stmt.caster = ClickHouse.types[stmt.name]
|
55
|
+
|
56
|
+
if stmt.caster.is_a?(Type::UndefinedType)
|
57
|
+
placeholders = stmt.arguments.map(&:placeholder)
|
58
|
+
stmt.caster = ClickHouse.types["#{stmt.name}(#{placeholders.join(', ')})"]
|
59
|
+
end
|
60
|
+
|
61
|
+
stmt.arguments.each(&method(:assign_type))
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param stmt [Ast::Statement]
|
65
|
+
def cast_type(stmt, value)
|
66
|
+
return cast_container(stmt, value) if stmt.caster.container?
|
67
|
+
return cast_map(stmt, Hash(value)) if stmt.caster.map?
|
68
|
+
return cast_tuple(stmt, Array(value)) if stmt.caster.tuple?
|
69
|
+
|
70
|
+
stmt.caster.cast(value, *stmt.arguments.map(&:value))
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Hash]
|
74
|
+
# @param stmt [Ast::Statement]
|
75
|
+
# @param hash [Hash]
|
76
|
+
def cast_map(stmt, hash)
|
77
|
+
raise ArgumentError, "expect hash got #{hash.class}" unless hash.is_a?(Hash)
|
78
|
+
|
79
|
+
key_type, value_type = stmt.arguments
|
80
|
+
hash.each_with_object({}) do |(key, value), object|
|
81
|
+
object[cast_type(key_type, key)] = cast_type(value_type, value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param stmt [Ast::Statement]
|
86
|
+
def cast_container(stmt, value)
|
87
|
+
stmt.caster.cast_each(value) do |item|
|
88
|
+
# TODO: raise an error if multiple arguments
|
89
|
+
cast_type(stmt.arguments.first, item)
|
90
|
+
end
|
91
|
+
end
|
74
92
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
93
|
+
# @param stmt [Ast::Statement]
|
94
|
+
def cast_tuple(stmt, value)
|
95
|
+
value.map.with_index do |item, ix|
|
96
|
+
cast_type(stmt.arguments.fetch(ix), item)
|
79
97
|
end
|
80
98
|
end
|
81
99
|
end
|
data/lib/click_house/response.rb
CHANGED
@@ -3,22 +3,16 @@
|
|
3
3
|
module ClickHouse
|
4
4
|
module Type
|
5
5
|
class ArrayType < BaseType
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(subtype)
|
9
|
-
@subtype = subtype
|
6
|
+
def cast_each(value, *_argv, &block)
|
7
|
+
value.map(&block)
|
10
8
|
end
|
11
9
|
|
12
|
-
def
|
13
|
-
|
10
|
+
def container?
|
11
|
+
true
|
14
12
|
end
|
15
13
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
array.map do |value|
|
20
|
-
subtype.serialize(value, *argv)
|
21
|
-
end
|
14
|
+
def ddl?
|
15
|
+
false
|
22
16
|
end
|
23
17
|
end
|
24
18
|
end
|