ch_connect 0.2.2 → 0.3.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/.standard.yml +5 -0
- data/CHANGELOG.md +10 -1
- data/README.md +156 -87
- data/ext/ch_connect_native/ch_connect_native.c +1001 -0
- data/ext/ch_connect_native/extconf.rb +32 -0
- data/lib/ch_connect/config.rb +60 -19
- data/lib/ch_connect/connection.rb +382 -13
- data/lib/ch_connect/response.rb +0 -8
- data/lib/ch_connect/version.rb +1 -1
- data/lib/ch_connect.rb +14 -11
- data/vendor/clickhouse-c/LICENSE +203 -0
- data/vendor/clickhouse-c/VENDOR.md +24 -0
- data/vendor/clickhouse-c/clickhouse-async.h +301 -0
- data/vendor/clickhouse-c/clickhouse-client.h +995 -0
- data/vendor/clickhouse-c/clickhouse-compression.h +634 -0
- data/vendor/clickhouse-c/clickhouse.h +3394 -0
- metadata +19 -14
- data/lib/ch_connect/body_reader.rb +0 -79
- data/lib/ch_connect/http_transport.rb +0 -59
- data/lib/ch_connect/native_format_parser.rb +0 -405
- data/lib/ch_connect/transport_result.rb +0 -12
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ch_connect
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Karol Bąk
|
|
@@ -24,41 +24,46 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '3.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: connection_pool
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '3.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
40
|
-
description: Fast Ruby client for ClickHouse
|
|
41
|
-
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: Fast Ruby client for ClickHouse using its native TCP protocol and binary
|
|
41
|
+
format
|
|
42
42
|
email:
|
|
43
43
|
- kukicola@gmail.com
|
|
44
44
|
executables: []
|
|
45
|
-
extensions:
|
|
45
|
+
extensions:
|
|
46
|
+
- ext/ch_connect_native/extconf.rb
|
|
46
47
|
extra_rdoc_files: []
|
|
47
48
|
files:
|
|
48
49
|
- ".rspec"
|
|
49
50
|
- ".standard.yml"
|
|
50
51
|
- CHANGELOG.md
|
|
51
52
|
- README.md
|
|
53
|
+
- ext/ch_connect_native/ch_connect_native.c
|
|
54
|
+
- ext/ch_connect_native/extconf.rb
|
|
52
55
|
- lib/ch_connect.rb
|
|
53
|
-
- lib/ch_connect/body_reader.rb
|
|
54
56
|
- lib/ch_connect/config.rb
|
|
55
57
|
- lib/ch_connect/connection.rb
|
|
56
|
-
- lib/ch_connect/http_transport.rb
|
|
57
|
-
- lib/ch_connect/native_format_parser.rb
|
|
58
58
|
- lib/ch_connect/null_instrumenter.rb
|
|
59
59
|
- lib/ch_connect/response.rb
|
|
60
|
-
- lib/ch_connect/transport_result.rb
|
|
61
60
|
- lib/ch_connect/version.rb
|
|
61
|
+
- vendor/clickhouse-c/LICENSE
|
|
62
|
+
- vendor/clickhouse-c/VENDOR.md
|
|
63
|
+
- vendor/clickhouse-c/clickhouse-async.h
|
|
64
|
+
- vendor/clickhouse-c/clickhouse-client.h
|
|
65
|
+
- vendor/clickhouse-c/clickhouse-compression.h
|
|
66
|
+
- vendor/clickhouse-c/clickhouse.h
|
|
62
67
|
homepage: https://github.com/kukicola/ch_connect
|
|
63
68
|
licenses:
|
|
64
69
|
- MIT
|
|
@@ -71,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
71
76
|
requirements:
|
|
72
77
|
- - ">="
|
|
73
78
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 3.
|
|
79
|
+
version: 3.2.0
|
|
75
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
81
|
requirements:
|
|
77
82
|
- - ">="
|
|
78
83
|
- !ruby/object:Gem::Version
|
|
79
84
|
version: '0'
|
|
80
85
|
requirements: []
|
|
81
|
-
rubygems_version:
|
|
86
|
+
rubygems_version: 4.0.6
|
|
82
87
|
specification_version: 4
|
|
83
|
-
summary: Ruby client for ClickHouse
|
|
88
|
+
summary: Ruby client for ClickHouse's native TCP protocol
|
|
84
89
|
test_files: []
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ChConnect
|
|
4
|
-
# Wrapper for HTTP response body providing buffered reads.
|
|
5
|
-
# Reads data in chunks for efficient small reads.
|
|
6
|
-
# @api private
|
|
7
|
-
class BodyReader
|
|
8
|
-
CHUNK_SIZE = 64 * 1024 # 64KB chunks
|
|
9
|
-
|
|
10
|
-
# Creates a new body reader.
|
|
11
|
-
#
|
|
12
|
-
# @param body [#read, #bytesize, #close] HTTP response body
|
|
13
|
-
def initialize(body)
|
|
14
|
-
@body = body
|
|
15
|
-
@size = body.bytesize
|
|
16
|
-
@buffer = "".b
|
|
17
|
-
@buffer_pos = 0
|
|
18
|
-
@eof = false
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Closes the underlying body.
|
|
22
|
-
#
|
|
23
|
-
# @return [void]
|
|
24
|
-
def close
|
|
25
|
-
@body.close
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Returns true if at end of stream.
|
|
29
|
-
#
|
|
30
|
-
# @return [Boolean]
|
|
31
|
-
def eof?
|
|
32
|
-
fill_buffer(1) if @buffer_pos >= @buffer.bytesize && !@eof
|
|
33
|
-
@eof && @buffer_pos >= @buffer.bytesize
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Reads exactly n bytes from the body.
|
|
37
|
-
#
|
|
38
|
-
# @param n [Integer] number of bytes to read
|
|
39
|
-
# @return [String] binary string of n bytes
|
|
40
|
-
def read(n)
|
|
41
|
-
fill_buffer(n)
|
|
42
|
-
result = @buffer.byteslice(@buffer_pos, n)
|
|
43
|
-
@buffer_pos += n
|
|
44
|
-
compact_buffer if @buffer_pos > CHUNK_SIZE
|
|
45
|
-
result
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Reads a single byte as integer, returns nil at EOF.
|
|
49
|
-
#
|
|
50
|
-
# @return [Integer, nil] byte value or nil at EOF
|
|
51
|
-
def getbyte
|
|
52
|
-
fill_buffer(1)
|
|
53
|
-
return nil if @buffer_pos >= @buffer.bytesize
|
|
54
|
-
|
|
55
|
-
byte = @buffer.getbyte(@buffer_pos)
|
|
56
|
-
@buffer_pos += 1
|
|
57
|
-
compact_buffer if @buffer_pos > CHUNK_SIZE
|
|
58
|
-
byte
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def fill_buffer(needed)
|
|
64
|
-
while !@eof && (@buffer.bytesize - @buffer_pos) < needed
|
|
65
|
-
chunk = @body.read(CHUNK_SIZE)
|
|
66
|
-
if chunk.nil? || chunk.empty?
|
|
67
|
-
@eof = true
|
|
68
|
-
else
|
|
69
|
-
@buffer << chunk
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def compact_buffer
|
|
75
|
-
@buffer = @buffer.byteslice(@buffer_pos..-1) || "".b
|
|
76
|
-
@buffer_pos = 0
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "httpx"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module ChConnect
|
|
7
|
-
# HTTP transport layer for ClickHouse communication.
|
|
8
|
-
# @api private
|
|
9
|
-
class HttpTransport
|
|
10
|
-
# Creates a new HTTP transport.
|
|
11
|
-
#
|
|
12
|
-
# @param config [Config] configuration instance
|
|
13
|
-
def initialize(config)
|
|
14
|
-
@config = config
|
|
15
|
-
@base_url = "#{config.scheme}://#{config.host}:#{config.port}"
|
|
16
|
-
@http_client = HTTPX.plugin(:persistent, close_on_fork: true)
|
|
17
|
-
.plugin(:retries, max_retries: config.max_retries, retry_change_requests: true)
|
|
18
|
-
.with(
|
|
19
|
-
timeout: {
|
|
20
|
-
connect_timeout: config.connection_timeout,
|
|
21
|
-
read_timeout: config.read_timeout,
|
|
22
|
-
write_timeout: config.write_timeout,
|
|
23
|
-
keep_alive_timeout: config.keep_alive_timeout
|
|
24
|
-
},
|
|
25
|
-
pool_options: {
|
|
26
|
-
max_connections_per_origin: config.pool_size,
|
|
27
|
-
pool_timeout: config.pool_timeout
|
|
28
|
-
}
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
@default_headers = {
|
|
32
|
-
"Accept-Encoding" => "gzip",
|
|
33
|
-
"X-ClickHouse-User" => config.username,
|
|
34
|
-
"X-ClickHouse-Key" => config.password,
|
|
35
|
-
"X-ClickHouse-Format" => "Native"
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Executes a SQL query via HTTP.
|
|
40
|
-
#
|
|
41
|
-
# @param sql [String] SQL query to execute
|
|
42
|
-
# @param options [Hash] query options
|
|
43
|
-
# @option options [Hash] :params query parameters
|
|
44
|
-
# @return [TransportResult] result containing body and summary
|
|
45
|
-
# @raise [QueryError] if the query fails
|
|
46
|
-
def execute(sql, options = {})
|
|
47
|
-
query_params = {database: @config.database}.merge(options[:params] || {})
|
|
48
|
-
response = @http_client.post(@base_url, params: query_params, body: sql, headers: @default_headers)
|
|
49
|
-
|
|
50
|
-
raise QueryError, response.error.message if response.error
|
|
51
|
-
|
|
52
|
-
summary = JSON.parse(response.headers["x-clickhouse-summary"], symbolize_names: true)
|
|
53
|
-
|
|
54
|
-
raise QueryError, response.body.to_s unless response.status == 200
|
|
55
|
-
|
|
56
|
-
TransportResult.new(body: response.body, summary: summary)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bigdecimal"
|
|
4
|
-
require "ipaddr"
|
|
5
|
-
|
|
6
|
-
module ChConnect
|
|
7
|
-
# Parser for ClickHouse Native binary format.
|
|
8
|
-
# @api private
|
|
9
|
-
class NativeFormatParser
|
|
10
|
-
DATE_EPOCH = Date.new(1970, 1, 1)
|
|
11
|
-
|
|
12
|
-
# Creates a new parser.
|
|
13
|
-
#
|
|
14
|
-
# @param body [#read] response body to parse
|
|
15
|
-
def initialize(body)
|
|
16
|
-
@reader = BodyReader.new(body)
|
|
17
|
-
@columns = []
|
|
18
|
-
@types = []
|
|
19
|
-
@rows = []
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Parses the response body and returns a Response.
|
|
23
|
-
#
|
|
24
|
-
# @return [Response] parsed response with columns, types, and rows
|
|
25
|
-
# @raise [UnsupportedTypeError] if an unsupported data type is encountered
|
|
26
|
-
def parse
|
|
27
|
-
parse_block until @reader.eof?
|
|
28
|
-
Response.new(columns: @columns, types: @types, rows: @rows)
|
|
29
|
-
ensure
|
|
30
|
-
@reader.close
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def parse_block
|
|
36
|
-
num_columns = read_varint
|
|
37
|
-
num_rows = read_varint
|
|
38
|
-
|
|
39
|
-
return if num_columns == 0 && num_rows == 0
|
|
40
|
-
|
|
41
|
-
columns_data = []
|
|
42
|
-
|
|
43
|
-
num_columns.times do
|
|
44
|
-
col_name = read_string
|
|
45
|
-
col_type = read_string
|
|
46
|
-
|
|
47
|
-
if @columns.length < num_columns
|
|
48
|
-
@columns << col_name.to_sym
|
|
49
|
-
@types << col_type.to_sym
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
columns_data << read_column(col_type, num_rows)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
@rows.concat(columns_data.transpose) if num_rows > 0
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def read_column(type, num_rows)
|
|
59
|
-
case type
|
|
60
|
-
# Integers
|
|
61
|
-
when "UInt8" then read_uint8_column(num_rows)
|
|
62
|
-
when "UInt16" then read_uint16_column(num_rows)
|
|
63
|
-
when "UInt32" then read_uint32_column(num_rows)
|
|
64
|
-
when "UInt64" then read_uint64_column(num_rows)
|
|
65
|
-
when "UInt128" then read_uint128_column(num_rows)
|
|
66
|
-
when "UInt256" then read_uint256_column(num_rows)
|
|
67
|
-
when "Int8" then read_int8_column(num_rows)
|
|
68
|
-
when "Int16" then read_int16_column(num_rows)
|
|
69
|
-
when "Int32" then read_int32_column(num_rows)
|
|
70
|
-
when "Int64" then read_int64_column(num_rows)
|
|
71
|
-
when "Int128" then read_int128_column(num_rows)
|
|
72
|
-
when "Int256" then read_int256_column(num_rows)
|
|
73
|
-
|
|
74
|
-
# Floats
|
|
75
|
-
when "Float32" then read_float32_column(num_rows)
|
|
76
|
-
when "Float64" then read_float64_column(num_rows)
|
|
77
|
-
|
|
78
|
-
# Boolean
|
|
79
|
-
when "Bool" then read_bool_column(num_rows)
|
|
80
|
-
|
|
81
|
-
# Strings
|
|
82
|
-
when "String" then read_string_column(num_rows)
|
|
83
|
-
when /^FixedString\((\d+)\)$/ then read_fixed_string_column($1.to_i, num_rows)
|
|
84
|
-
|
|
85
|
-
# Dates and Times
|
|
86
|
-
when "Date" then read_date_column(num_rows)
|
|
87
|
-
when "Date32" then read_date32_column(num_rows)
|
|
88
|
-
when "DateTime", /^DateTime\(.+\)$/ then read_datetime_column(num_rows)
|
|
89
|
-
when /^DateTime64\((\d+)(?:,.*)?\)$/ then read_datetime64_column($1.to_i, num_rows)
|
|
90
|
-
|
|
91
|
-
# UUID
|
|
92
|
-
when "UUID" then read_uuid_column(num_rows)
|
|
93
|
-
|
|
94
|
-
# IP addresses
|
|
95
|
-
when "IPv4" then read_ipv4_column(num_rows)
|
|
96
|
-
when "IPv6" then read_ipv6_column(num_rows)
|
|
97
|
-
|
|
98
|
-
# Decimals - ClickHouse always returns Decimal(precision, scale)
|
|
99
|
-
when /^Decimal\((\d+),\s*(\d+)\)$/ then read_decimal_column($1.to_i, $2.to_i, num_rows)
|
|
100
|
-
|
|
101
|
-
# Enums (stored as signed integers)
|
|
102
|
-
when /^Enum8\(.+\)$/ then read_int8_column(num_rows)
|
|
103
|
-
when /^Enum16\(.+\)$/ then read_int16_column(num_rows)
|
|
104
|
-
|
|
105
|
-
# Nullable
|
|
106
|
-
when /^Nullable\((.+)\)$/ then read_nullable_column($1, num_rows)
|
|
107
|
-
|
|
108
|
-
# LowCardinality
|
|
109
|
-
when /^LowCardinality\((.+)\)$/ then read_low_cardinality_column($1, num_rows)
|
|
110
|
-
|
|
111
|
-
# Arrays
|
|
112
|
-
when /^Array\((.+)\)$/ then read_array_column($1, num_rows)
|
|
113
|
-
|
|
114
|
-
# Tuples
|
|
115
|
-
when /^Tuple\((.*)\)$/ then read_tuple_column(parse_tuple_types($1), num_rows)
|
|
116
|
-
|
|
117
|
-
# Maps
|
|
118
|
-
when /^Map\((.+)\)$/
|
|
119
|
-
types = parse_tuple_types($1)
|
|
120
|
-
read_map_column(types[0], types[1], num_rows)
|
|
121
|
-
|
|
122
|
-
else
|
|
123
|
-
raise UnsupportedTypeError, "Unsupported column type: #{type}"
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# --- Bulk Column Readers ---
|
|
128
|
-
|
|
129
|
-
def read_uint8_column(num_rows)
|
|
130
|
-
@reader.read(num_rows).bytes
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def read_uint16_column(num_rows)
|
|
134
|
-
@reader.read(num_rows * 2).unpack("v*")
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def read_uint32_column(num_rows)
|
|
138
|
-
@reader.read(num_rows * 4).unpack("V*")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def read_uint64_column(num_rows)
|
|
142
|
-
@reader.read(num_rows * 8).unpack("Q<*")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def read_uint128_column(num_rows)
|
|
146
|
-
Array.new(num_rows) { read_le_bytes(16) }
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def read_uint256_column(num_rows)
|
|
150
|
-
Array.new(num_rows) { read_le_bytes(32) }
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def read_int8_column(num_rows)
|
|
154
|
-
@reader.read(num_rows).unpack("c*")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def read_int16_column(num_rows)
|
|
158
|
-
@reader.read(num_rows * 2).unpack("s<*")
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def read_int32_column(num_rows)
|
|
162
|
-
@reader.read(num_rows * 4).unpack("l<*")
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def read_int64_column(num_rows)
|
|
166
|
-
@reader.read(num_rows * 8).unpack("q<*")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def read_int128_column(num_rows)
|
|
170
|
-
Array.new(num_rows) { read_signed_le_bytes(16) }
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def read_int256_column(num_rows)
|
|
174
|
-
Array.new(num_rows) { read_signed_le_bytes(32) }
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def read_float32_column(num_rows)
|
|
178
|
-
@reader.read(num_rows * 4).unpack("e*")
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def read_float64_column(num_rows)
|
|
182
|
-
@reader.read(num_rows * 8).unpack("E*")
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def read_bool_column(num_rows)
|
|
186
|
-
@reader.read(num_rows).bytes.map { |b| b == 1 }
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def read_string_column(num_rows)
|
|
190
|
-
Array.new(num_rows) { read_string }
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def read_fixed_string_column(length, num_rows)
|
|
194
|
-
Array.new(num_rows) { @reader.read(length).force_encoding(Encoding::UTF_8) }
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def read_date_column(num_rows)
|
|
198
|
-
@reader.read(num_rows * 2).unpack("v*").map { |days| DATE_EPOCH + days }
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def read_date32_column(num_rows)
|
|
202
|
-
@reader.read(num_rows * 4).unpack("l<*").map { |days| DATE_EPOCH + days }
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def read_datetime_column(num_rows)
|
|
206
|
-
@reader.read(num_rows * 4).unpack("V*").map { |ts| Time.at(ts).utc }
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def read_datetime64_column(precision, num_rows)
|
|
210
|
-
scale = 10**(9 - precision)
|
|
211
|
-
@reader.read(num_rows * 8).unpack("q<*").map do |ticks|
|
|
212
|
-
nsec = ticks * scale
|
|
213
|
-
Time.at(nsec / 1_000_000_000, nsec % 1_000_000_000, :nanosecond).utc
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def read_uuid_column(num_rows)
|
|
218
|
-
Array.new(num_rows) { read_uuid }
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def read_ipv4_column(num_rows)
|
|
222
|
-
Array.new(num_rows) { read_ipv4 }
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def read_ipv6_column(num_rows)
|
|
226
|
-
Array.new(num_rows) { read_ipv6 }
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def read_decimal_column(precision, scale, num_rows)
|
|
230
|
-
divisor = 10**scale
|
|
231
|
-
if precision <= 9
|
|
232
|
-
@reader.read(num_rows * 4).unpack("l<*").map { |v| BigDecimal(v) / divisor }
|
|
233
|
-
elsif precision <= 18
|
|
234
|
-
@reader.read(num_rows * 8).unpack("q<*").map { |v| BigDecimal(v) / divisor }
|
|
235
|
-
elsif precision <= 38
|
|
236
|
-
Array.new(num_rows) { BigDecimal(read_signed_le_bytes(16)) / divisor }
|
|
237
|
-
else
|
|
238
|
-
Array.new(num_rows) { BigDecimal(read_signed_le_bytes(32)) / divisor }
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
# --- Single Value Readers ---
|
|
243
|
-
|
|
244
|
-
def read_varint
|
|
245
|
-
result = 0
|
|
246
|
-
shift = 0
|
|
247
|
-
loop do
|
|
248
|
-
byte = @reader.getbyte
|
|
249
|
-
return result if byte.nil?
|
|
250
|
-
result |= (byte & 0x7F) << shift
|
|
251
|
-
break if (byte & 0x80) == 0
|
|
252
|
-
shift += 7
|
|
253
|
-
end
|
|
254
|
-
result
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def read_string
|
|
258
|
-
@reader.read(read_varint).force_encoding(Encoding::UTF_8)
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def read_uint64 = @reader.read(8).unpack1("Q<")
|
|
262
|
-
|
|
263
|
-
def read_uuid
|
|
264
|
-
first_half = @reader.read(8).bytes.reverse
|
|
265
|
-
second_half = @reader.read(8).bytes.reverse
|
|
266
|
-
hex = (first_half + second_half).pack("C*").unpack1("H*")
|
|
267
|
-
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def read_ipv4
|
|
271
|
-
bytes = @reader.read(4).unpack("C4").reverse
|
|
272
|
-
IPAddr.new(bytes.join("."))
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def read_ipv6
|
|
276
|
-
bytes = @reader.read(16)
|
|
277
|
-
IPAddr.new(bytes.unpack1("H*").scan(/.{4}/).join(":"), Socket::AF_INET6)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# --- Container Type Readers ---
|
|
281
|
-
|
|
282
|
-
# Nullable: nulls mask (uint8 per row, 1=null), then all values
|
|
283
|
-
def read_nullable_column(inner_type, num_rows)
|
|
284
|
-
nulls = @reader.read(num_rows).bytes
|
|
285
|
-
values = read_column(inner_type, num_rows)
|
|
286
|
-
num_rows.times { |i| values[i] = nil if nulls[i] == 1 }
|
|
287
|
-
values
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
# Array: cumulative offsets (uint64 per row), then all elements
|
|
291
|
-
def read_array_column(inner_type, num_rows)
|
|
292
|
-
offsets = read_uint64_column(num_rows)
|
|
293
|
-
total_elements = offsets.last || 0
|
|
294
|
-
|
|
295
|
-
return Array.new(num_rows) { [] } if total_elements == 0
|
|
296
|
-
|
|
297
|
-
elements = read_column(inner_type, total_elements)
|
|
298
|
-
|
|
299
|
-
arrays = Array.new(num_rows)
|
|
300
|
-
prev_offset = 0
|
|
301
|
-
offsets.each_with_index do |offset, i|
|
|
302
|
-
arrays[i] = elements.slice(prev_offset, offset - prev_offset)
|
|
303
|
-
prev_offset = offset
|
|
304
|
-
end
|
|
305
|
-
arrays
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Map: cumulative offsets (uint64 per row), then all keys, then all values
|
|
309
|
-
def read_map_column(key_type, value_type, num_rows)
|
|
310
|
-
offsets = read_uint64_column(num_rows)
|
|
311
|
-
total_pairs = offsets.last || 0
|
|
312
|
-
|
|
313
|
-
return Array.new(num_rows) { {} } if total_pairs == 0
|
|
314
|
-
|
|
315
|
-
keys = read_column(key_type, total_pairs)
|
|
316
|
-
values = read_column(value_type, total_pairs)
|
|
317
|
-
|
|
318
|
-
maps = Array.new(num_rows)
|
|
319
|
-
prev_offset = 0
|
|
320
|
-
offsets.each_with_index do |offset, i|
|
|
321
|
-
len = offset - prev_offset
|
|
322
|
-
maps[i] = keys.slice(prev_offset, len).zip(values.slice(prev_offset, len)).to_h
|
|
323
|
-
prev_offset = offset
|
|
324
|
-
end
|
|
325
|
-
maps
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Tuple: all values of element 0, then element 1, etc. (column-major)
|
|
329
|
-
# Empty tuples send 1 byte per row.
|
|
330
|
-
def read_tuple_column(element_types, num_rows)
|
|
331
|
-
if element_types.empty?
|
|
332
|
-
@reader.read(num_rows)
|
|
333
|
-
return Array.new(num_rows) { [] }
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
element_columns = element_types.map { |type| read_column(type, num_rows) }
|
|
337
|
-
|
|
338
|
-
Array.new(num_rows) { |i| element_columns.map { |col| col[i] } }
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
# LowCardinality: version, meta, dictionary, keys
|
|
342
|
-
def read_low_cardinality_column(inner_type, num_rows)
|
|
343
|
-
_version = read_uint64
|
|
344
|
-
meta = read_uint64
|
|
345
|
-
key_type = meta & 0xFF
|
|
346
|
-
|
|
347
|
-
dict_size = read_uint64
|
|
348
|
-
dictionary = read_column(inner_type, dict_size)
|
|
349
|
-
|
|
350
|
-
_num_keys = read_uint64
|
|
351
|
-
keys = case key_type
|
|
352
|
-
when 0 then read_uint8_column(num_rows)
|
|
353
|
-
when 1 then read_uint16_column(num_rows)
|
|
354
|
-
when 2 then read_uint32_column(num_rows)
|
|
355
|
-
else read_uint64_column(num_rows)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
keys.map { |k| dictionary[k] }
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# --- Helpers ---
|
|
362
|
-
|
|
363
|
-
def read_le_bytes(num_bytes)
|
|
364
|
-
bytes = @reader.read(num_bytes).bytes
|
|
365
|
-
result = 0
|
|
366
|
-
bytes.each_with_index { |b, i| result |= b << (8 * i) }
|
|
367
|
-
result
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def read_signed_le_bytes(num_bytes)
|
|
371
|
-
value = read_le_bytes(num_bytes)
|
|
372
|
-
max_positive = 1 << (num_bytes * 8 - 1)
|
|
373
|
-
(value >= max_positive) ? value - (1 << (num_bytes * 8)) : value
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def parse_tuple_types(types_str)
|
|
377
|
-
types = []
|
|
378
|
-
depth = 0
|
|
379
|
-
current = +""
|
|
380
|
-
|
|
381
|
-
types_str.each_char do |c|
|
|
382
|
-
case c
|
|
383
|
-
when "("
|
|
384
|
-
depth += 1
|
|
385
|
-
current << c
|
|
386
|
-
when ")"
|
|
387
|
-
depth -= 1
|
|
388
|
-
current << c
|
|
389
|
-
when ","
|
|
390
|
-
if depth == 0
|
|
391
|
-
types << current.strip
|
|
392
|
-
current = +""
|
|
393
|
-
else
|
|
394
|
-
current << c
|
|
395
|
-
end
|
|
396
|
-
else
|
|
397
|
-
current << c
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
types << current.strip unless current.empty?
|
|
402
|
-
types
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ChConnect
|
|
4
|
-
# Immutable result from transport layer.
|
|
5
|
-
# @api private
|
|
6
|
-
#
|
|
7
|
-
# @!attribute [r] body
|
|
8
|
-
# @return [HTTP::Response::Body] response body
|
|
9
|
-
# @!attribute [r] summary
|
|
10
|
-
# @return [Hash] ClickHouse query summary
|
|
11
|
-
TransportResult = Data.define(:body, :summary)
|
|
12
|
-
end
|