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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1975451cfb97c84b89c7153fcd18a8057477ea5fd3a13a21a0b11463343b124a
4
- data.tar.gz: e5182056af628032f2faf8b8f17732870d8c89e6d16fdddc1ccbf70742cfc649
3
+ metadata.gz: 2b03e3eeb6a94bc7bbef9469f177e5a1c50cbd186bb9b83fd0782acfe047ef88
4
+ data.tar.gz: e7e1ab0e6c1749164c471c8d86ae5fd133fff5609589a5ae112ed579028e3d8c
5
5
  SHA512:
6
- metadata.gz: 64540c1ebc919c74a9f69c738fa47d8d81eab54b39e4b862a3b9651aa0763563cd6cc362739008a7e1020fb0c608f902fcd97e56fd60971b3661d2525958cc33
7
- data.tar.gz: 2d064557ed0ce3c84e3e9b0f2ad27b1d05c660d3312842d6ece5ce6e3a56329d08829bd4abdeda06cc2fa4570d86b2ccd7eefcec91037c699387bb60de6a4ebb
6
+ metadata.gz: 0cad454a3045989417a620d0bb23d9de4b38d22c6991efa01cb6a29ebb25b8530380b914b36f1c8c2b3eb4c565d48ff2969ddbff8da3fe1156d92da6951ef484
7
+ data.tar.gz: cbef85609d1c8a182a4fbf4f41322da86b5f843d6bac8a2539af4e57af01fbed44e11071b675cbf72590fbdd938c3d3a64bfc80e94187cfea4cf7b8ec74ba692
data/.standard.yml CHANGED
@@ -1,3 +1,8 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
3
  ruby_version: 3.4
4
+
5
+ ignore:
6
+ # mkmf's API is built on global variables ($INCFLAGS, $LDFLAGS, $defs)
7
+ - "ext/**/extconf.rb":
8
+ - Style/GlobalVars
data/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
- ## Unreleased
1
+ ## [0.3.0] - 2026-08-11
2
+
3
+ - Switched from HTTP to ClickHouse's native TCP protocol for faster queries and lower allocations
4
+ - Added connection pooling, LZ4/ZSTD compression, TLS, and native ClickHouse URLs
5
+ - Added native query parameters and per-query ClickHouse settings
6
+ - Added support for Ruby array query parameters
7
+ - Added opt-in `idempotent: true` query retries for transport failures on fresh pooled connections
8
+ - Added configurable capped exponential backoff with jitter between connection retries
9
+ - Improved connection timeouts, retries, fork safety, and idle connection handling
10
+ - Removed HTTP configuration; existing connections must use native ports, normally 9000 or 9440
2
11
 
3
12
  ## [0.2.2] - 2026-05-06
4
13
 
data/README.md CHANGED
@@ -1,78 +1,96 @@
1
1
  # ch_connect
2
2
 
3
- > **Note:** This gem was previously published as `clickhouse-rb` and has been renamed to `ch_connect` due to name conflicts.
3
+ > **Note:** This gem was previously published as `clickhouse-rb` and was renamed to `ch_connect` due to name conflicts.
4
4
 
5
- Fast Ruby client for ClickHouse database using the Native binary format for efficient data transfer.
5
+ Fast MRI Ruby client for ClickHouse's native TCP protocol.
6
6
 
7
7
  ## Features
8
8
 
9
- - Native binary format parsing (faster than JSON/TSV)
10
- - Persistent HTTP connections with built-in connection pooling
11
- - Automatic retries on connection errors
12
- - Thread-safe concurrent access
13
- - Supports all common ClickHouse data types
9
+ - Native TCP protocol and binary result decoding
10
+ - Persistent pooled connections safe for concurrent use
11
+ - LZ4 or ZSTD block compression
12
+ - TLS with certificate verification
13
+ - Safe connection-establishment retries and opt-in retries for idempotent queries
14
+ - Interruptible connect, read, and write timeouts
15
+ - Common ClickHouse scalar and composite types
14
16
 
15
17
  ## Benchmarks
16
18
 
17
- Compared against other Ruby ClickHouse gems ([click_house](https://github.com/shlima/click_house), [clickhouse](https://github.com/archan937/clickhouse), [click_house-client](https://gitlab.com/gitlab-org/ruby/gems/clickhouse-client)) on Ruby 3.4.3:
19
+ Compared with [click_house](https://github.com/shlima/click_house), [clickhouse](https://github.com/archan937/clickhouse), and [click_house-client](https://gitlab.com/gitlab-org/ruby/gems/clickhouse-client) on Ruby 4.0.3. `ch_connect` uses its default LZ4 compression.
18
20
 
19
- **Speed (iterations/second, higher is better):**
21
+ Speed (iterations/second, higher is better):
20
22
 
21
23
  | Scenario | ch_connect | click_house | clickhouse | click_house-client |
22
24
  |----------|------------|-------------|------------|--------------------|
23
- | Small queries (10 rows) | **680 i/s** | 342 i/s (2.0x slower) | 293 i/s (2.3x slower) | 346 i/s (2.0x slower) |
24
- | Large queries (100K rows) | **3.5 i/s** | 1.1 i/s (3.3x slower) | 0.5 i/s (6.9x slower) | 1.6 i/s (2.2x slower) |
25
+ | Small queries (10 rows) | **962 i/s** | 363 i/s (2.7x slower) | 317 i/s (3.0x slower) | 372 i/s (2.6x slower) |
26
+ | Large queries (100K rows) | **8.2 i/s** | 1.1 i/s (7.2x slower) | 0.6 i/s (14.1x slower) | 1.5 i/s (5.3x slower) |
25
27
 
26
- **Memory (large query, lower is better):**
28
+ Multi-threaded throughput (10K-row query, total queries/second):
29
+
30
+ | Threads | ch_connect | click_house | clickhouse | click_house-client |
31
+ |---------|------------|-------------|------------|--------------------|
32
+ | 1 | **292 q/s** | 69 q/s | 117 q/s | 48 q/s |
33
+ | 4 | **598 q/s** | 162 q/s | 319 q/s | 82 q/s |
34
+ | 8 | **606 q/s** | 158 q/s | 321 q/s | 80 q/s |
35
+
36
+ Memory allocated for a large query (lower is better):
27
37
 
28
38
  | Gem | Allocated |
29
39
  |-----|-----------|
30
- | ch_connect | **130 MB** |
31
- | click_house | 205 MB (1.6x more) |
32
- | clickhouse | 483 MB (3.7x more) |
33
- | click_house-client | 210 MB (1.6x more) |
40
+ | ch_connect | **97 MB** |
41
+ | click_house | 198 MB |
42
+ | click_house-client | 210 MB |
43
+ | clickhouse | 436 MB |
34
44
 
35
- See `benchmark/` directory for full benchmark suite and methodology.
45
+ A small query allocates approximately **3.2 KB / 32 Ruby objects**. See the [`benchmark/`](benchmark/) directory for the comparison suite and methodology.
36
46
 
37
47
  ## Installation
38
48
 
39
- Add to your Gemfile:
49
+ Add the gem to your Gemfile:
40
50
 
41
51
  ```ruby
42
52
  gem "ch_connect"
43
53
  ```
44
54
 
45
- Then run:
46
-
47
- ```bash
48
- bundle install
49
- ```
50
-
51
- ## Usage
55
+ The gem contains a native extension and currently requires MRI Ruby. LZ4 and ZSTD support are detected when the extension is built.
52
56
 
53
- ### Configuration
57
+ ## Configuration
54
58
 
55
59
  ```ruby
56
60
  require "ch_connect"
57
61
 
58
62
  ChConnect.configure do |config|
59
63
  config.host = "localhost"
60
- config.port = 8123
64
+ config.port = 9000
61
65
  config.database = "default"
62
66
  config.username = "default"
63
67
  config.password = ""
64
68
  end
65
69
  ```
66
70
 
67
- Or configure via URL:
71
+ Or use a native ClickHouse URL:
72
+
73
+ ```ruby
74
+ ChConnect.configure do |config|
75
+ config.url = "clickhouse://user:pass@localhost:9000/mydb"
76
+ end
77
+ ```
78
+
79
+ Supported URL schemes:
80
+
81
+ - `clickhouse://` and `tcp://`: plaintext TCP, default port 9000
82
+ - `clickhouses://` and `tcps://`: TLS, default port 9440
68
83
 
69
84
  ```ruby
70
85
  ChConnect.configure do |config|
71
- config.url = "http://user:pass@localhost:8123/mydb"
86
+ config.url = "clickhouses://user:pass@clickhouse.example.com:9440/mydb"
87
+ config.ssl_ca = "/path/to/ca.crt" # optional; system roots are the default
72
88
  end
73
89
  ```
74
90
 
75
- ### Single Connection
91
+ HTTP URLs and ClickHouse's HTTP endpoint are not supported.
92
+
93
+ ## Querying
76
94
 
77
95
  ```ruby
78
96
  conn = ChConnect::Connection.new
@@ -83,9 +101,7 @@ response.each do |row|
83
101
  end
84
102
  ```
85
103
 
86
- ### Thread-Safe Usage
87
-
88
- Connections use httpx's built-in connection pooling, making them safe for concurrent use:
104
+ A `Connection` owns a connection pool and is safe for concurrent use:
89
105
 
90
106
  ```ruby
91
107
  conn = ChConnect::Connection.new
@@ -96,59 +112,96 @@ end
96
112
  threads.each(&:join)
97
113
  ```
98
114
 
99
- Pool settings are configured globally:
115
+ Configuration is copied when a `Connection` is created. Create a new connection
116
+ to apply configuration changes.
117
+
118
+ ### Idempotent retries
119
+
120
+ Queries are not retried after execution may have started unless the caller
121
+ explicitly marks the operation as idempotent:
100
122
 
101
123
  ```ruby
102
- ChConnect.configure do |config|
103
- config.pool_size = 10
104
- config.pool_timeout = 5
105
- end
124
+ response = conn.query(
125
+ "SELECT * FROM users WHERE id = {id:UInt64}",
126
+ params: {id: 123},
127
+ idempotent: true
128
+ )
106
129
  ```
107
130
 
108
- ### Working with Results
131
+ With `idempotent: true`, a transport `ChConnect::ConnectionError` is retried on
132
+ a fresh pooled connection up to `max_retries`. Connection-establishment
133
+ failures continue to use the same retry limit for every query because no query
134
+ has been sent. Retries use exponential backoff with jitter, starting at
135
+ `retry_base_interval` and capped at `retry_max_interval`; set the base interval
136
+ to `0` to disable the delay. `ChConnect::QueryError` is never retried.
109
137
 
110
- Response objects implement `Enumerable`, allowing direct iteration:
138
+ Only opt in when repeating the complete operation is safe. In particular,
139
+ writes remain non-retried by default because a lost response does not prove
140
+ that ClickHouse did not apply the write.
141
+
142
+ ### Results
143
+
144
+ `Response` implements `Enumerable`:
111
145
 
112
146
  ```ruby
113
147
  response = conn.query("SELECT id, name, created_at FROM users")
114
148
 
115
- # Iterate over rows as hashes with symbol keys
116
149
  response.each { |row| puts row[:name] }
117
-
118
- # Use any Enumerable method
119
150
  response.map { |row| row[:id] }
120
- response.select { |row| row[:id] > 10 }
121
- response.first # => {id: 1, name: "Alice", created_at: 2024-01-01 00:00:00 UTC}
151
+ response.first
122
152
 
123
- # Access raw rows (arrays)
124
- response.rows # => [[1, "Alice", 2024-01-01 00:00:00 UTC], ...]
125
- response.columns # => [:id, :name, :created_at]
126
- response.types # => [:UInt64, :String, :DateTime]
153
+ response.rows # [[1, "Alice", 2024-01-01 00:00:00 UTC], ...]
154
+ response.columns # [:id, :name, :created_at]
155
+ response.types # [:UInt64, :String, :DateTime]
156
+ response.to_a # [{id: 1, name: "Alice", ...}, ...]
157
+ response.summary # {read_rows: 1, read_bytes: 42, ...}
158
+ ```
159
+
160
+ `summary[:client_elapsed_ns]` is client wall-clock time for the complete query,
161
+ including pool checkout and connection establishment. Native responses do not
162
+ currently expose the HTTP transport's former server-side `elapsed_ns` metric.
127
163
 
128
- # Convert to array of hashes
129
- response.to_a # => [{id: 1, name: "Alice", ...}, ...]
164
+ ### Query parameters
130
165
 
131
- # Query summary from ClickHouse (symbol keys)
132
- response.summary # => {read_rows: "1", read_bytes: "42", ...}
166
+ ```ruby
167
+ response = conn.query(
168
+ "SELECT * FROM users WHERE id = {id:UInt64}",
169
+ params: {id: 123}
170
+ )
171
+
172
+ response = conn.query(
173
+ "SELECT * FROM users WHERE tag IN {tags:Array(String)}",
174
+ params: {tags: ["ruby", "clickhouse"]}
175
+ )
133
176
  ```
134
177
 
135
- ### Query Parameters
178
+ `nil` is encoded as the native nullable marker.
179
+ Parameter keys match the names used in query placeholders. Ruby arrays are
180
+ serialized recursively and support strings, symbols, numbers, dates, booleans,
181
+ `nil`, and nested arrays. Other element types raise `ArgumentError`; format
182
+ values such as `Time` and `DateTime` as strings for the target ClickHouse type.
183
+
184
+ ### Query settings
185
+
186
+ Any ClickHouse setting can be supplied per query:
136
187
 
137
188
  ```ruby
138
189
  response = conn.query(
139
- "SELECT * FROM users WHERE id = {id:UInt64}",
140
- params: { param_id: 123 }
190
+ "SELECT * FROM big_table",
191
+ settings: {
192
+ max_threads: 2,
193
+ max_result_rows: 10_000,
194
+ result_overflow_mode: "break"
195
+ }
141
196
  )
142
197
  ```
143
198
 
144
- ## Supported Data Types
199
+ ## Supported data types
145
200
 
146
- | ClickHouse Type | Ruby Type |
201
+ | ClickHouse type | Ruby type |
147
202
  |-----------------|-----------|
148
- | UInt8/16/32/64 | Integer |
149
- | UInt128/256 | Integer |
150
- | Int8/16/32/64 | Integer |
151
- | Int128/256 | Integer |
203
+ | UInt8/16/32/64/128/256 | Integer |
204
+ | Int8/16/32/64/128/256 | Integer |
152
205
  | Float32/64 | Float |
153
206
  | Decimal | BigDecimal |
154
207
  | Bool | TrueClass/FalseClass |
@@ -164,66 +217,82 @@ response = conn.query(
164
217
  | Nullable | nil or inner type |
165
218
  | LowCardinality | inner type |
166
219
 
167
- ## Configuration Options
220
+ Unsupported types raise `ChConnect::UnsupportedTypeError` and discard the affected pooled connection.
221
+
222
+ ## Configuration options
168
223
 
169
224
  | Option | Default | Description |
170
225
  |--------|---------|-------------|
171
- | `scheme` | `"http"` | URL scheme (http/https) |
172
226
  | `host` | `"localhost"` | ClickHouse server host |
173
- | `port` | `8123` | ClickHouse HTTP port |
227
+ | `port` | `9000` | Native TCP port (`9440` when TLS is enabled before port resolution) |
228
+ | `compression` | `:lz4` | `:lz4`, `:zstd`, or `nil` |
229
+ | `ssl` | `false` | Enable TLS |
230
+ | `ssl_verify` | `true` | Verify the server certificate |
231
+ | `ssl_ca` | `nil` | CA certificate file; system roots are used by default |
174
232
  | `database` | `"default"` | Database name |
175
- | `username` | `""` | Authentication username |
233
+ | `username` | `"default"` | Authentication username |
176
234
  | `password` | `""` | Authentication password |
177
- | `connection_timeout` | `5` | Connection timeout in seconds |
178
- | `read_timeout` | `60` | Read timeout in seconds |
179
- | `write_timeout` | `60` | Write timeout in seconds |
180
- | `keep_alive_timeout` | `8` | Idle persistent connection timeout in seconds |
181
- | `pool_size` | `100` | Connection pool size |
235
+ | `connection_timeout` | `5` | TCP and protocol handshake timeout in seconds |
236
+ | `read_timeout` | `60` | Maximum time without response data |
237
+ | `write_timeout` | `60` | Socket write deadline in seconds |
238
+ | `keep_alive_timeout` | `60` | Recycle pooled TCP connections after this many idle seconds; `nil` disables recycling |
239
+ | `pool_size` | `100` | Maximum pooled TCP connections |
182
240
  | `pool_timeout` | `5` | Pool checkout timeout in seconds |
183
- | `max_retries` | `3` | Max retry attempts on connection errors (0 to disable) |
184
- | `instrumenter` | `NullInstrumenter` | Instrumenter for query instrumentation |
241
+ | `max_retries` | `3` | Retry limit for connection establishment and opted-in idempotent query transport failures |
242
+ | `retry_base_interval` | `0.05` | Initial retry delay in seconds; retries use exponential backoff with jitter, and `0` disables the delay |
243
+ | `retry_max_interval` | `1.0` | Maximum retry delay in seconds |
244
+ | `instrumenter` | `NullInstrumenter` | Object responding to `instrument` |
185
245
 
186
246
  ## Instrumentation
187
247
 
188
- You can instrument queries by providing an instrumenter that responds to `#instrument`:
189
-
190
248
  ```ruby
191
249
  ChConnect.configure do |config|
192
250
  config.instrumenter = ActiveSupport::Notifications
193
251
  end
194
252
 
195
- # Subscribe to events
196
253
  ActiveSupport::Notifications.subscribe("query.clickhouse") do |name, start, finish, id, payload|
197
254
  puts "Query: #{payload[:sql]} took #{finish - start}s"
198
255
  end
199
256
  ```
200
257
 
201
- The instrumenter receives event name `"query.clickhouse"` and payload `{sql: "..."}`.
202
-
203
- ## Error Handling
258
+ ## Error handling
204
259
 
205
260
  ```ruby
206
261
  begin
207
262
  conn.query("INVALID SQL")
208
- rescue ChConnect::QueryError => e
209
- puts "Query failed: #{e.message}"
263
+ rescue ChConnect::QueryError => error
264
+ warn error.message
210
265
  end
211
266
 
212
- # Unsupported types raise an exception
213
267
  begin
214
268
  conn.query("SELECT '{}'::JSON")
215
- rescue ChConnect::UnsupportedTypeError => e
216
- puts "Unsupported type: #{e.message}"
269
+ rescue ChConnect::UnsupportedTypeError => error
270
+ warn error.message
217
271
  end
218
272
  ```
219
273
 
274
+ Network, timeout, pool, and protocol failures raise `ChConnect::ConnectionError`.
275
+ They are retried after a query may have started only when `idempotent: true`;
276
+ server `ChConnect::QueryError` responses are never retried.
277
+ When migrating from the HTTP client, change port 8123/8443 to the server's
278
+ native port (normally 9000/9440) and replace `scheme` with `ssl` or a native URL.
279
+
220
280
  ## Development
221
281
 
222
282
  ```bash
223
- # Run tests (requires ClickHouse)
224
- CLICKHOUSE_URL=http://default:password@localhost:8123/default bundle exec rspec
283
+ cd ext/ch_connect_native
284
+ ruby extconf.rb
285
+ make
286
+ cp "ch_connect_native.$(ruby -rrbconfig -e 'print RbConfig::CONFIG["DLEXT"]')" ../../lib/ch_connect/
287
+ cd ../..
288
+
289
+ CLICKHOUSE_URL=clickhouse://default:password@localhost:9000/default bundle exec rspec
290
+
291
+ # Optional TLS specs
292
+ spec/support/start_tls_clickhouse.sh /tmp/ch-tls password
293
+ CH_TLS_PORT=9440 CH_TLS_CA=/tmp/ch-tls/server.crt \
294
+ CLICKHOUSE_URL=clickhouse://default:password@localhost:9000/default bundle exec rspec
225
295
 
226
- # Run linter
227
296
  bundle exec standardrb
228
297
  ```
229
298