clickhouse-rb 0.2.0 → 0.4.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/CHANGELOG.md +10 -0
- data/README.md +28 -17
- data/lib/clickhouse/body_reader.rb +40 -0
- data/lib/clickhouse/config.rb +8 -2
- data/lib/clickhouse/http_transport.rb +19 -8
- data/lib/clickhouse/native_format_parser.rb +48 -20
- data/lib/clickhouse/response.rb +15 -8
- data/lib/clickhouse/version.rb +1 -1
- data/lib/clickhouse.rb +1 -2
- metadata +5 -20
- data/lib/clickhouse/buffered_reader.rb +0 -69
- data/lib/clickhouse/pool.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86a862148595c1c9794a3d122e750db7959f5d4478b9dc3f65ee0f00319c55c0
|
|
4
|
+
data.tar.gz: c346972569d9e190445eceea9c9c7eef9bffa8d0dea95825ed53c1025e52e74a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a31b2bcd62a7157933af6e3e57a28b3314a9b42fd91c8250304f77a2eb8eb74c4df5651f1952ff76e237aa586eae77135e8d84dc24672e42eba9a95c2c42c899
|
|
7
|
+
data.tar.gz: '09479b26facc3ae8584d584cd1d2982c3aa46f0c43cffa5a0ec91baa93d036647ce5a910224f51f17ed26c157755e8f5eed3fbbf3d0994a849086258d269d866'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## [0.4.0] - 2026-01-25
|
|
4
|
+
|
|
5
|
+
- Replace http.rb with httpx for HTTP client, remove `Pool` class ([#3](https://github.com/kukicola/clickhouse-rb/pull/3))
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2025-12-31
|
|
8
|
+
|
|
9
|
+
- Make responses more user-friendly ([#1](https://github.com/kukicola/clickhouse-rb/pull/1))
|
|
10
|
+
|
|
1
11
|
## [0.2.0] - 2025-12-28
|
|
2
12
|
|
|
3
13
|
- Added instrumentation support with configurable `instrumenter` (defaults to `NullInstrumenter`)
|
data/README.md
CHANGED
|
@@ -5,8 +5,8 @@ Fast Ruby client for ClickHouse database using the Native binary format for effi
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Native binary format parsing (faster than JSON/TSV)
|
|
8
|
-
- Persistent HTTP connections
|
|
9
|
-
-
|
|
8
|
+
- Persistent HTTP connections with built-in connection pooling
|
|
9
|
+
- Thread-safe concurrent access
|
|
10
10
|
- Supports all common ClickHouse data types
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
@@ -51,51 +51,60 @@ end
|
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
53
|
conn = Clickhouse::Connection.new
|
|
54
|
-
response = conn.query("SELECT
|
|
54
|
+
response = conn.query("SELECT id, name FROM users WHERE active = true")
|
|
55
55
|
|
|
56
|
-
response.
|
|
57
|
-
puts row
|
|
56
|
+
response.each do |row|
|
|
57
|
+
puts "#{row[:id]}: #{row[:name]}"
|
|
58
58
|
end
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
###
|
|
61
|
+
### Thread-Safe Usage
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Connections use httpx's built-in connection pooling, making them safe for concurrent use:
|
|
64
64
|
|
|
65
65
|
```ruby
|
|
66
|
-
|
|
66
|
+
conn = Clickhouse::Connection.new
|
|
67
67
|
|
|
68
|
-
# Thread-safe queries
|
|
69
68
|
threads = 10.times.map do
|
|
70
|
-
Thread.new {
|
|
69
|
+
Thread.new { conn.query("SELECT 1") }
|
|
71
70
|
end
|
|
72
71
|
threads.each(&:join)
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
Pool
|
|
74
|
+
Pool settings are configured globally:
|
|
76
75
|
|
|
77
76
|
```ruby
|
|
78
77
|
Clickhouse.configure do |config|
|
|
79
|
-
config.pool_size = 10
|
|
78
|
+
config.pool_size = 10
|
|
80
79
|
config.pool_timeout = 5
|
|
81
80
|
end
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
### Working with Results
|
|
85
84
|
|
|
85
|
+
Response objects implement `Enumerable`, allowing direct iteration:
|
|
86
|
+
|
|
86
87
|
```ruby
|
|
87
88
|
response = conn.query("SELECT id, name, created_at FROM users")
|
|
88
89
|
|
|
90
|
+
# Iterate over rows as hashes with symbol keys
|
|
91
|
+
response.each { |row| puts row[:name] }
|
|
92
|
+
|
|
93
|
+
# Use any Enumerable method
|
|
94
|
+
response.map { |row| row[:id] }
|
|
95
|
+
response.select { |row| row[:id] > 10 }
|
|
96
|
+
response.first # => {id: 1, name: "Alice", created_at: 2024-01-01 00:00:00 UTC}
|
|
97
|
+
|
|
89
98
|
# Access raw rows (arrays)
|
|
90
99
|
response.rows # => [[1, "Alice", 2024-01-01 00:00:00 UTC], ...]
|
|
91
|
-
response.columns # => [
|
|
92
|
-
response.types # => [
|
|
100
|
+
response.columns # => [:id, :name, :created_at]
|
|
101
|
+
response.types # => [:UInt64, :String, :DateTime]
|
|
93
102
|
|
|
94
103
|
# Convert to array of hashes
|
|
95
|
-
response.to_a # => [{
|
|
104
|
+
response.to_a # => [{id: 1, name: "Alice", ...}, ...]
|
|
96
105
|
|
|
97
|
-
# Query summary from ClickHouse
|
|
98
|
-
response.summary # => {
|
|
106
|
+
# Query summary from ClickHouse (symbol keys)
|
|
107
|
+
response.summary # => {read_rows: "1", read_bytes: "42", ...}
|
|
99
108
|
```
|
|
100
109
|
|
|
101
110
|
### Query Parameters
|
|
@@ -141,6 +150,8 @@ response = conn.query(
|
|
|
141
150
|
| `username` | `""` | Authentication username |
|
|
142
151
|
| `password` | `""` | Authentication password |
|
|
143
152
|
| `connection_timeout` | `5` | Connection timeout in seconds |
|
|
153
|
+
| `read_timeout` | `60` | Read timeout in seconds |
|
|
154
|
+
| `write_timeout` | `60` | Write timeout in seconds |
|
|
144
155
|
| `pool_size` | `100` | Connection pool size |
|
|
145
156
|
| `pool_timeout` | `5` | Pool checkout timeout in seconds |
|
|
146
157
|
| `instrumenter` | `NullInstrumenter` | Instrumenter for query instrumentation |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickhouse
|
|
4
|
+
# Wrapper for HTTP response body providing position tracking and EOF detection.
|
|
5
|
+
# @api private
|
|
6
|
+
class BodyReader
|
|
7
|
+
# Creates a new body reader.
|
|
8
|
+
#
|
|
9
|
+
# @param body [#read, #bytesize, #close] HTTP response body
|
|
10
|
+
def initialize(body)
|
|
11
|
+
@body = body
|
|
12
|
+
@pos = 0
|
|
13
|
+
@size = body.bytesize
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Closes the underlying body.
|
|
17
|
+
#
|
|
18
|
+
# @return [void]
|
|
19
|
+
def close
|
|
20
|
+
@body.close
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns true if at end of stream.
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def eof?
|
|
27
|
+
@pos >= @size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Reads exactly n bytes from the body.
|
|
31
|
+
#
|
|
32
|
+
# @param n [Integer] number of bytes to read
|
|
33
|
+
# @return [String] binary string of n bytes
|
|
34
|
+
def read(n)
|
|
35
|
+
result = @body.read(n)
|
|
36
|
+
@pos += n
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/clickhouse/config.rb
CHANGED
|
@@ -20,6 +20,8 @@ module Clickhouse
|
|
|
20
20
|
username: "",
|
|
21
21
|
password: "",
|
|
22
22
|
connection_timeout: 5,
|
|
23
|
+
read_timeout: 60,
|
|
24
|
+
write_timeout: 60,
|
|
23
25
|
pool_size: 100,
|
|
24
26
|
pool_timeout: 5,
|
|
25
27
|
instrumenter: NullInstrumenter.new
|
|
@@ -32,10 +34,12 @@ module Clickhouse
|
|
|
32
34
|
# @return [String] Username for authentication
|
|
33
35
|
# @return [String] Password for authentication
|
|
34
36
|
# @return [Integer] Connection timeout in seconds
|
|
37
|
+
# @return [Integer] Read timeout in seconds
|
|
38
|
+
# @return [Integer] Write timeout in seconds
|
|
35
39
|
# @return [Integer] Connection pool size
|
|
36
40
|
# @return [Integer] Pool checkout timeout in seconds
|
|
37
41
|
# @return [#instrument] Instrumenter for query instrumentation
|
|
38
|
-
attr_accessor :scheme, :host, :port, :database, :username, :password, :connection_timeout, :pool_size, :pool_timeout, :instrumenter
|
|
42
|
+
attr_accessor :scheme, :host, :port, :database, :username, :password, :connection_timeout, :read_timeout, :write_timeout, :pool_size, :pool_timeout, :instrumenter
|
|
39
43
|
|
|
40
44
|
# Creates a new configuration instance.
|
|
41
45
|
#
|
|
@@ -46,7 +50,9 @@ module Clickhouse
|
|
|
46
50
|
# @option params [String] :database database name (default: "default")
|
|
47
51
|
# @option params [String] :username authentication username (default: "")
|
|
48
52
|
# @option params [String] :password authentication password (default: "")
|
|
49
|
-
# @option params [Integer] :connection_timeout timeout in seconds (default: 5)
|
|
53
|
+
# @option params [Integer] :connection_timeout connection timeout in seconds (default: 5)
|
|
54
|
+
# @option params [Integer] :read_timeout read timeout in seconds (default: 60)
|
|
55
|
+
# @option params [Integer] :write_timeout write timeout in seconds (default: 60)
|
|
50
56
|
# @option params [Integer] :pool_size connection pool size (default: 100)
|
|
51
57
|
# @option params [Integer] :pool_timeout pool checkout timeout (default: 5)
|
|
52
58
|
def initialize(params = {})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "httpx"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module Clickhouse
|
|
@@ -12,10 +12,19 @@ module Clickhouse
|
|
|
12
12
|
# @param config [Config] configuration instance
|
|
13
13
|
def initialize(config)
|
|
14
14
|
@config = config
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
15
|
+
@base_url = "#{config.scheme}://#{config.host}:#{config.port}"
|
|
16
|
+
@http_client = HTTPX.plugin(:persistent, close_on_fork: true)
|
|
17
|
+
.with(
|
|
18
|
+
timeout: {
|
|
19
|
+
connect_timeout: config.connection_timeout,
|
|
20
|
+
read_timeout: config.read_timeout,
|
|
21
|
+
write_timeout: config.write_timeout
|
|
22
|
+
},
|
|
23
|
+
pool_options: {
|
|
24
|
+
max_connections_per_origin: config.pool_size,
|
|
25
|
+
pool_timeout: config.pool_timeout
|
|
26
|
+
}
|
|
27
|
+
)
|
|
19
28
|
|
|
20
29
|
@default_headers = {
|
|
21
30
|
"Accept-Encoding" => "gzip",
|
|
@@ -34,11 +43,13 @@ module Clickhouse
|
|
|
34
43
|
# @raise [QueryError] if the query fails
|
|
35
44
|
def execute(sql, options = {})
|
|
36
45
|
query_params = {database: @config.database}.merge(options[:params] || {})
|
|
37
|
-
response = @http_client.post(
|
|
46
|
+
response = @http_client.post(@base_url, params: query_params, body: sql, headers: @default_headers)
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
raise QueryError, response.error.message if response.error
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
summary = JSON.parse(response.headers["x-clickhouse-summary"], symbolize_names: true)
|
|
51
|
+
|
|
52
|
+
raise QueryError, response.body.to_s unless response.status == 200
|
|
42
53
|
|
|
43
54
|
TransportResult.new(body: response.body, summary: summary)
|
|
44
55
|
end
|
|
@@ -11,9 +11,9 @@ module Clickhouse
|
|
|
11
11
|
|
|
12
12
|
# Creates a new parser.
|
|
13
13
|
#
|
|
14
|
-
# @param body [#
|
|
14
|
+
# @param body [#read] response body to parse
|
|
15
15
|
def initialize(body)
|
|
16
|
-
@
|
|
16
|
+
@reader = BodyReader.new(body)
|
|
17
17
|
@columns = []
|
|
18
18
|
@types = []
|
|
19
19
|
@rows = []
|
|
@@ -24,11 +24,10 @@ module Clickhouse
|
|
|
24
24
|
# @return [Response] parsed response with columns, types, and rows
|
|
25
25
|
# @raise [UnsupportedTypeError] if an unsupported data type is encountered
|
|
26
26
|
def parse
|
|
27
|
-
@reader = BufferedReader.new(@body)
|
|
28
27
|
parse_block until @reader.eof?
|
|
29
28
|
Response.new(columns: @columns, types: @types, rows: @rows)
|
|
30
29
|
ensure
|
|
31
|
-
@reader.
|
|
30
|
+
@reader.close
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
private
|
|
@@ -46,8 +45,8 @@ module Clickhouse
|
|
|
46
45
|
col_type = read_string
|
|
47
46
|
|
|
48
47
|
if @columns.length < num_columns
|
|
49
|
-
@columns << col_name
|
|
50
|
-
@types << col_type
|
|
48
|
+
@columns << col_name.to_sym
|
|
49
|
+
@types << col_type.to_sym
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
columns_data << read_column(col_type, num_rows)
|
|
@@ -65,14 +64,14 @@ module Clickhouse
|
|
|
65
64
|
when "UInt16" then read_uint16_column(num_rows)
|
|
66
65
|
when "UInt32" then read_uint32_column(num_rows)
|
|
67
66
|
when "UInt64" then read_uint64_column(num_rows)
|
|
68
|
-
when "UInt128" then
|
|
69
|
-
when "UInt256" then
|
|
67
|
+
when "UInt128" then read_uint128_column(num_rows)
|
|
68
|
+
when "UInt256" then read_uint256_column(num_rows)
|
|
70
69
|
when "Int8" then read_int8_column(num_rows)
|
|
71
70
|
when "Int16" then read_int16_column(num_rows)
|
|
72
71
|
when "Int32" then read_int32_column(num_rows)
|
|
73
72
|
when "Int64" then read_int64_column(num_rows)
|
|
74
|
-
when "Int128" then
|
|
75
|
-
when "Int256" then
|
|
73
|
+
when "Int128" then read_int128_column(num_rows)
|
|
74
|
+
when "Int256" then read_int256_column(num_rows)
|
|
76
75
|
|
|
77
76
|
# Floats
|
|
78
77
|
when "Float32" then read_float32_column(num_rows)
|
|
@@ -82,7 +81,7 @@ module Clickhouse
|
|
|
82
81
|
when "Bool" then read_bool_column(num_rows)
|
|
83
82
|
|
|
84
83
|
# Strings
|
|
85
|
-
when "String" then
|
|
84
|
+
when "String" then read_string_column(num_rows)
|
|
86
85
|
when /^FixedString\((\d+)\)$/ then read_fixed_string_column($1.to_i, num_rows)
|
|
87
86
|
|
|
88
87
|
# Dates and Times
|
|
@@ -92,11 +91,11 @@ module Clickhouse
|
|
|
92
91
|
when /^DateTime64\((\d+)(?:,.*)?\)$/ then read_datetime64_column($1.to_i, num_rows)
|
|
93
92
|
|
|
94
93
|
# UUID
|
|
95
|
-
when "UUID" then
|
|
94
|
+
when "UUID" then read_uuid_column(num_rows)
|
|
96
95
|
|
|
97
96
|
# IP addresses
|
|
98
|
-
when "IPv4" then
|
|
99
|
-
when "IPv6" then
|
|
97
|
+
when "IPv4" then read_ipv4_column(num_rows)
|
|
98
|
+
when "IPv6" then read_ipv6_column(num_rows)
|
|
100
99
|
|
|
101
100
|
# Decimals - ClickHouse always returns Decimal(precision, scale)
|
|
102
101
|
when /^Decimal\((\d+),\s*(\d+)\)$/ then read_decimal_column($1.to_i, $2.to_i, num_rows)
|
|
@@ -145,6 +144,14 @@ module Clickhouse
|
|
|
145
144
|
@reader.read(num_rows * 8).unpack("Q<*")
|
|
146
145
|
end
|
|
147
146
|
|
|
147
|
+
def read_uint128_column(num_rows)
|
|
148
|
+
Array.new(num_rows) { read_le_bytes(16) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def read_uint256_column(num_rows)
|
|
152
|
+
Array.new(num_rows) { read_le_bytes(32) }
|
|
153
|
+
end
|
|
154
|
+
|
|
148
155
|
def read_int8_column(num_rows)
|
|
149
156
|
@reader.read(num_rows).unpack("c*")
|
|
150
157
|
end
|
|
@@ -161,6 +168,14 @@ module Clickhouse
|
|
|
161
168
|
@reader.read(num_rows * 8).unpack("q<*")
|
|
162
169
|
end
|
|
163
170
|
|
|
171
|
+
def read_int128_column(num_rows)
|
|
172
|
+
Array.new(num_rows) { read_signed_le_bytes(16) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def read_int256_column(num_rows)
|
|
176
|
+
Array.new(num_rows) { read_signed_le_bytes(32) }
|
|
177
|
+
end
|
|
178
|
+
|
|
164
179
|
def read_float32_column(num_rows)
|
|
165
180
|
@reader.read(num_rows * 4).unpack("e*")
|
|
166
181
|
end
|
|
@@ -173,6 +188,10 @@ module Clickhouse
|
|
|
173
188
|
@reader.read(num_rows).bytes.map { |b| b == 1 }
|
|
174
189
|
end
|
|
175
190
|
|
|
191
|
+
def read_string_column(num_rows)
|
|
192
|
+
Array.new(num_rows) { read_string }
|
|
193
|
+
end
|
|
194
|
+
|
|
176
195
|
def read_fixed_string_column(length, num_rows)
|
|
177
196
|
Array.new(num_rows) { @reader.read(length).force_encoding(Encoding::UTF_8) }
|
|
178
197
|
end
|
|
@@ -197,6 +216,18 @@ module Clickhouse
|
|
|
197
216
|
end
|
|
198
217
|
end
|
|
199
218
|
|
|
219
|
+
def read_uuid_column(num_rows)
|
|
220
|
+
Array.new(num_rows) { read_uuid }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def read_ipv4_column(num_rows)
|
|
224
|
+
Array.new(num_rows) { read_ipv4 }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def read_ipv6_column(num_rows)
|
|
228
|
+
Array.new(num_rows) { read_ipv6 }
|
|
229
|
+
end
|
|
230
|
+
|
|
200
231
|
def read_decimal_column(precision, scale, num_rows)
|
|
201
232
|
divisor = 10**scale
|
|
202
233
|
if precision <= 9
|
|
@@ -216,8 +247,9 @@ module Clickhouse
|
|
|
216
247
|
result = 0
|
|
217
248
|
shift = 0
|
|
218
249
|
loop do
|
|
219
|
-
|
|
220
|
-
return result if
|
|
250
|
+
byte_str = @reader.read(1)
|
|
251
|
+
return result if byte_str.nil? || byte_str.empty?
|
|
252
|
+
byte = byte_str.ord
|
|
221
253
|
result |= (byte & 0x7F) << shift
|
|
222
254
|
break if (byte & 0x80) == 0
|
|
223
255
|
shift += 7
|
|
@@ -230,10 +262,6 @@ module Clickhouse
|
|
|
230
262
|
end
|
|
231
263
|
|
|
232
264
|
def read_uint64 = @reader.read(8).unpack1("Q<")
|
|
233
|
-
def read_uint128 = read_le_bytes(16)
|
|
234
|
-
def read_uint256 = read_le_bytes(32)
|
|
235
|
-
def read_int128 = read_signed_le_bytes(16)
|
|
236
|
-
def read_int256 = read_signed_le_bytes(32)
|
|
237
265
|
|
|
238
266
|
def read_uuid
|
|
239
267
|
first_half = @reader.read(8).bytes.reverse
|
data/lib/clickhouse/response.rb
CHANGED
|
@@ -5,20 +5,27 @@ module Clickhouse
|
|
|
5
5
|
#
|
|
6
6
|
# @example
|
|
7
7
|
# response = conn.query("SELECT id, name FROM users")
|
|
8
|
-
# response.columns # => [
|
|
8
|
+
# response.columns # => [:id, :name]
|
|
9
9
|
# response.rows # => [[1, "Alice"], [2, "Bob"]]
|
|
10
|
-
# response.
|
|
10
|
+
# response.each { |row| puts row[:name] }
|
|
11
11
|
Response = Data.define(:columns, :types, :rows, :summary) do
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
# @param columns [Array<Symbol>] column names
|
|
15
|
+
# @param types [Array<Symbol>] column types
|
|
14
16
|
# @param rows [Array<Array>] row data
|
|
15
|
-
# @param summary [Hash, nil] ClickHouse query summary
|
|
17
|
+
# @param summary [Hash, nil] ClickHouse query summary with symbol keys
|
|
16
18
|
def initialize(columns: [], types: [], rows: [], summary: nil)
|
|
17
19
|
super
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
#
|
|
21
|
-
# @
|
|
22
|
-
|
|
22
|
+
# Iterates over rows as hashes with symbol keys.
|
|
23
|
+
# @yield [Hash] each row as a hash
|
|
24
|
+
# @return [Enumerator] if no block given
|
|
25
|
+
def each
|
|
26
|
+
return to_enum(:each) unless block_given?
|
|
27
|
+
|
|
28
|
+
rows.each { |row| yield columns.zip(row).to_h }
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
end
|
data/lib/clickhouse/version.rb
CHANGED
data/lib/clickhouse.rb
CHANGED
|
@@ -6,9 +6,8 @@ require_relative "clickhouse/config"
|
|
|
6
6
|
require_relative "clickhouse/transport_result"
|
|
7
7
|
require_relative "clickhouse/http_transport"
|
|
8
8
|
require_relative "clickhouse/connection"
|
|
9
|
-
require_relative "clickhouse/pool"
|
|
10
9
|
require_relative "clickhouse/response"
|
|
11
|
-
require_relative "clickhouse/
|
|
10
|
+
require_relative "clickhouse/body_reader"
|
|
12
11
|
require_relative "clickhouse/native_format_parser"
|
|
13
12
|
|
|
14
13
|
# Ruby client for ClickHouse database with Native format support.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clickhouse-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Karol Bąk
|
|
@@ -24,33 +24,19 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '3.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: httpx
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '1.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
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: http
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '5.0'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '5.0'
|
|
39
|
+
version: '1.0'
|
|
54
40
|
description: Fast Ruby client for ClickHouse database using the Native binary format
|
|
55
41
|
for efficient data transfer
|
|
56
42
|
email:
|
|
@@ -65,13 +51,12 @@ files:
|
|
|
65
51
|
- README.md
|
|
66
52
|
- lib/clickhouse-rb.rb
|
|
67
53
|
- lib/clickhouse.rb
|
|
68
|
-
- lib/clickhouse/
|
|
54
|
+
- lib/clickhouse/body_reader.rb
|
|
69
55
|
- lib/clickhouse/config.rb
|
|
70
56
|
- lib/clickhouse/connection.rb
|
|
71
57
|
- lib/clickhouse/http_transport.rb
|
|
72
58
|
- lib/clickhouse/native_format_parser.rb
|
|
73
59
|
- lib/clickhouse/null_instrumenter.rb
|
|
74
|
-
- lib/clickhouse/pool.rb
|
|
75
60
|
- lib/clickhouse/response.rb
|
|
76
61
|
- lib/clickhouse/transport_result.rb
|
|
77
62
|
- lib/clickhouse/version.rb
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clickhouse
|
|
4
|
-
# Buffered binary reader for streaming IO.
|
|
5
|
-
# @api private
|
|
6
|
-
class BufferedReader
|
|
7
|
-
# Creates a new buffered reader.
|
|
8
|
-
#
|
|
9
|
-
# @param io [#readpartial] IO object supporting readpartial
|
|
10
|
-
def initialize(io)
|
|
11
|
-
@io = io
|
|
12
|
-
@buffer = String.new(encoding: Encoding::BINARY)
|
|
13
|
-
@eof = false
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Returns true if at end of stream.
|
|
17
|
-
#
|
|
18
|
-
# @return [Boolean]
|
|
19
|
-
def eof?
|
|
20
|
-
fill if @buffer.empty?
|
|
21
|
-
@buffer.empty?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Reads exactly n bytes from the stream.
|
|
25
|
-
#
|
|
26
|
-
# @param n [Integer] number of bytes to read
|
|
27
|
-
# @return [String] binary string of n bytes
|
|
28
|
-
def read(n)
|
|
29
|
-
fill until @buffer.bytesize >= n
|
|
30
|
-
@buffer.slice!(0, n)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Reads a single byte from the stream.
|
|
34
|
-
#
|
|
35
|
-
# @return [Integer, nil] byte value (0-255) or nil if at EOF
|
|
36
|
-
def read_byte
|
|
37
|
-
fill if @buffer.empty?
|
|
38
|
-
return if @buffer.empty?
|
|
39
|
-
|
|
40
|
-
@buffer.slice!(0, 1).ord
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Drains remaining data from the stream.
|
|
44
|
-
#
|
|
45
|
-
# @return [void]
|
|
46
|
-
def flush
|
|
47
|
-
@buffer.clear
|
|
48
|
-
nil while @io.readpartial
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def fill
|
|
54
|
-
return if @eof
|
|
55
|
-
|
|
56
|
-
loop do
|
|
57
|
-
chunk = @io.readpartial
|
|
58
|
-
if chunk.nil?
|
|
59
|
-
@eof = true
|
|
60
|
-
break
|
|
61
|
-
elsif !chunk.empty?
|
|
62
|
-
@buffer << chunk
|
|
63
|
-
break
|
|
64
|
-
end
|
|
65
|
-
# Empty chunk - keep reading (gzip header processing)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
data/lib/clickhouse/pool.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "connection_pool"
|
|
4
|
-
|
|
5
|
-
module Clickhouse
|
|
6
|
-
# Thread-safe connection pool for ClickHouse.
|
|
7
|
-
#
|
|
8
|
-
# @example
|
|
9
|
-
# pool = Clickhouse::Pool.new
|
|
10
|
-
# response = pool.query("SELECT * FROM users")
|
|
11
|
-
#
|
|
12
|
-
# @example Concurrent usage
|
|
13
|
-
# pool = Clickhouse::Pool.new
|
|
14
|
-
# threads = 10.times.map do
|
|
15
|
-
# Thread.new { pool.query("SELECT 1") }
|
|
16
|
-
# end
|
|
17
|
-
# threads.each(&:join)
|
|
18
|
-
class Pool
|
|
19
|
-
# Creates a new connection pool.
|
|
20
|
-
#
|
|
21
|
-
# @param config [Config] configuration instance (defaults to global config)
|
|
22
|
-
# Pool size and timeout are read from config.pool_size and config.pool_timeout
|
|
23
|
-
def initialize(config = Clickhouse.config)
|
|
24
|
-
@pool = ConnectionPool.new(size: config.pool_size, timeout: config.pool_timeout) do
|
|
25
|
-
Connection.new(config)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Executes a SQL query using a pooled connection.
|
|
30
|
-
#
|
|
31
|
-
# @param sql [String] SQL query to execute
|
|
32
|
-
# @param options [Hash] query options
|
|
33
|
-
# @option options [Hash] :params query parameters
|
|
34
|
-
# @return [Response] query response with rows, columns, and metadata
|
|
35
|
-
def query(sql, options = {})
|
|
36
|
-
@pool.with { |conn| conn.query(sql, options) }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|