click_house-client 0.10.0 → 0.12.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: bef646c7c07c30559265378e9fec3f14f5e41aeadd86058120a31d532d77d10a
4
- data.tar.gz: e969763ad335eda82d36f3d949207cfcb95ab238646ff0b3e4ec18e5a17bd445
3
+ metadata.gz: 5513d15d8bf17295cf98d021b30650f46d0101a6d828b2620499f87a949f2e3d
4
+ data.tar.gz: 27fd28cc07d8b67d8de72031de4e3c7e343cef51a3230afd96ba0adf3d24c252
5
5
  SHA512:
6
- metadata.gz: 9f7883a21fba0ab5534c85ca43686a3490234252c2277e200a244a3bead56fa3a31d7597c8b23deecb0086b5ad3a90dde67c8425f8ce257c289cb29abb8b29ad
7
- data.tar.gz: 175aaeaefe0f02f0ba1cd9e3828d9d4bb876c450f626765b78568297c0d35d7e2eb7de00f900ea88db84f7240fbb32790fededbd258dcad4afb4d9291bb16f7c
6
+ metadata.gz: b3d7bf7328dd7ab42b004e3e35e58c35b4869267e4a2e701f7d29f963c2c445c3d63f0bbd5e45db679cc4d2fda6880ef367aff18e0a04b1a030f54ede2317ead
7
+ data.tar.gz: 40125fc9d75f13be2f1230ea872b72c6d01e797384674064d3fb42c613d1792c827e58d6ccfcd894b783ff4d89b838c04e9f50b2e297f4b085bba5c33b95585e
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- click_house-client (0.10.0)
4
+ click_house-client (0.11.0)
5
5
  activerecord (>= 7.0, < 9.0)
6
6
  activesupport (>= 7.0, < 9.0)
7
7
  addressable (~> 2.8)
@@ -136,7 +136,7 @@ CHECKSUMS
136
136
  benchmark (0.4.1) sha256=d4ef40037bba27f03b28013e219b950b82bace296549ec15a78016552f8d2cce
137
137
  bigdecimal (3.2.2) sha256=39085f76b495eb39a79ce07af716f3a6829bc35eb44f2195e2753749f2fa5adc
138
138
  byebug (12.0.0) sha256=d4a150d291cca40b66ec9ca31f754e93fed8aa266a17335f71bb0afa7fca1a1e
139
- click_house-client (0.10.0)
139
+ click_house-client (0.11.0)
140
140
  concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6
141
141
  connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b
142
142
  diff-lcs (1.5.0) sha256=49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ClickHouse::Client
2
2
 
3
- This Gem provides a simple way to query ClickHouse databases using the HTTP interface.
3
+ This Gem provides a simple way to query ClickHouse databases using the HTTP interface.
4
4
 
5
5
  ## Example usage
6
6
 
@@ -61,6 +61,21 @@ puts ClickHouse::Client.execute('CREATE TABLE IF NOT EXISTS t1 (id Int64) ENGINE
61
61
  puts ClickHouse::Client.execute('DROP TABLE IF EXISTS t1', :main)
62
62
  ```
63
63
 
64
+ ## Custom typecasters
65
+
66
+ Values returned by `ClickHouse::Client.select` are typecast based on the column type using built-in typecasters (integers, floats, dates, datetimes, intervals). You can register custom typecasters which take precedence over the built-in ones:
67
+
68
+ ```ruby
69
+ ClickHouse::Client.configure do |config|
70
+ # Parse the value of `*_json` String columns as JSON
71
+ config.register_typecaster('String', ->(meta, value) do
72
+ meta['name'].match?(/\w+_json\z/) ? JSON.parse(value) : value
73
+ end)
74
+ end
75
+ ```
76
+
77
+ The first argument is the column type: either a `String` for an exact match or a `Regexp` (for example `/\AU?Int\d+\z/`). Wrapper types such as `Nullable(...)`, `LowCardinality(...)` and `SimpleAggregateFunction(...)` are unwrapped before matching. The typecaster receives the column metadata Hash (for example `{ 'name' => 'title', 'type' => 'String' }`) and the raw value. It should handle its own edge cases (for example malformed JSON). `NULL` values are returned as `nil` without invoking any typecaster.
78
+
64
79
  ## ClickHouse::Client::QueryBuilder
65
80
 
66
81
  The QueryBuilder provides an ActiveRecord-like interface for constructing ClickHouse queries programmatically. While similar to ActiveRecord's query interface, it has been tailored specifically for ClickHouse's SQL dialect and features.
@@ -182,6 +197,24 @@ query
182
197
  # => "SELECT * FROM `users` WHERE `users`.`active` = 'true' GROUP BY `users`.`department` HAVING `users`.`avg_salary` > 50000"
183
198
  ```
184
199
 
200
+ ### Reversing Order
201
+
202
+ The `reverse_order` method inverts the direction of all `ORDER BY` clauses (`ASC` becomes `DESC` and vice versa).
203
+
204
+ ```ruby
205
+ query.order(:name, :desc).order(:id).reverse_order.to_sql
206
+ # => "SELECT * FROM `users` ORDER BY `users`.`name` ASC, `users`.`id` DESC"
207
+ ```
208
+
209
+ ### Empty Result Set
210
+
211
+ Use `.none` to get a query that yields no rows. It appends an always-false `1 = 0` condition to the `WHERE` clause and stays chainable with other query builder methods:
212
+
213
+ ```ruby
214
+ query.none.where(active: true).to_sql
215
+ # => "SELECT * FROM `users` WHERE 1 = 0 AND `users`.`active` = 'true'"
216
+ ```
217
+
185
218
  ### FINAL Modifier
186
219
 
187
220
  ClickHouse's `FINAL` modifier forces merging of rows during query time for tables in the MergeTree family. Apply it via `.final`:
@@ -233,6 +266,57 @@ query
233
266
  # => "SELECT * FROM `users` LEFT OUTER JOIN (SELECT `orders`.`id`, `orders`.`user_id` FROM `orders`) `o` ON `users`.`id` = `o`.`user_id`"
234
267
  ```
235
268
 
269
+ ### Common Table Expressions (CTEs)
270
+
271
+ Use `#as_cte(name)` to wrap a query as a named CTE node, then attach it to a
272
+ main query with `#with(cte)`. The CTE can then be referenced by name in `FROM`
273
+ and IN-subquery positions, which is useful when the same subquery is needed in
274
+ more than one place.
275
+
276
+ ```ruby
277
+ inner = ClickHouse::Client::QueryBuilder.new('builds').select(:id, :stage_id)
278
+ query_builder = ClickHouse::Client::QueryBuilder.new('finished_builds').select(:id)
279
+
280
+ query_builder.with(inner.as_cte(:finished_builds)).to_sql
281
+ # => "WITH finished_builds AS (SELECT `builds`.`id`, `builds`.`stage_id` FROM `builds`) SELECT `finished_builds`.`id` FROM `finished_builds`"
282
+ ```
283
+
284
+ Chained calls accumulate, unlike `Arel::SelectManager#with` which would keep
285
+ only the last CTE:
286
+
287
+ ```ruby
288
+ query_builder.with(inner.as_cte(:a)).with(inner.as_cte(:b)).to_sql
289
+ # => "WITH a AS (...), b AS (...) SELECT ..."
290
+ ```
291
+
292
+ `#with` returns a new `QueryBuilder` (immutable), requires an `Arel::Nodes::Cte`
293
+ (build it via `#as_cte`), and raises if the same CTE name is declared more than
294
+ once.
295
+
296
+ ### UNION
297
+
298
+ Use `#union` to combine the result sets of two or more queries. The combined
299
+ queries are wrapped as an aliased subquery, so further chaining (`where`,
300
+ `order`, `limit`, ...) applies to the outer `SELECT`.
301
+
302
+ ```ruby
303
+ query1 = ClickHouse::Client::QueryBuilder.new('users_2023').select(:id)
304
+ query2 = ClickHouse::Client::QueryBuilder.new('users_2024').select(:id)
305
+
306
+ query1.union(query2).to_sql
307
+ # => "SELECT * FROM ( SELECT `users_2023`.`id` FROM `users_2023` UNION DISTINCT SELECT `users_2024`.`id` FROM `users_2024` ) `union_subquery`"
308
+
309
+ # UNION ALL with a custom subquery alias, chaining on the union result
310
+ query1.union(query2, type: :all, alias_name: 'users').where(id: [1, 2]).to_sql
311
+ # => "SELECT * FROM ( ... UNION ALL ... ) `users` WHERE `users`.`id` IN (1, 2)"
312
+
313
+ # More than two queries can be combined in a single call
314
+ query1.union(query2, query3, type: :all)
315
+ ```
316
+
317
+ `#union` returns a new `QueryBuilder` (immutable) and raises if given anything
318
+ other than `QueryBuilder` instances or an unknown `type`.
319
+
236
320
  ### Complete Example
237
321
 
238
322
  Here's a comprehensive example combining multiple QueryBuilder features:
@@ -260,12 +344,12 @@ result = ClickHouse::Client::QueryBuilder.new('users')
260
344
  .limit(10)
261
345
 
262
346
  puts result.to_sql
263
- "SELECT `users`.`department`, COUNT(*) AS user_count FROM `users` WHERE `users`.`active` = 'true'
264
- AND `users`.`department` IN ('Sales', 'Marketing', 'Engineering')
265
- AND `users`.`id` IN (SELECT `orders`.`user_id` FROM `orders` WHERE `orders`.`status` = 'completed'
266
- AND `users`.`created_at` > '2025-08-12')
267
- AND `users`.`email` ILIKE '%@company.com'
268
- GROUP BY department HAVING COUNT(*) AS user_count > 5
347
+ "SELECT `users`.`department`, COUNT(*) AS user_count FROM `users` WHERE `users`.`active` = 'true'
348
+ AND `users`.`department` IN ('Sales', 'Marketing', 'Engineering')
349
+ AND `users`.`id` IN (SELECT `orders`.`user_id` FROM `orders` WHERE `orders`.`status` = 'completed'
350
+ AND `users`.`created_at` > '2025-08-12')
351
+ AND `users`.`email` ILIKE '%@company.com'
352
+ GROUP BY department HAVING COUNT(*) AS user_count > 5
269
353
  ORDER BY user_count DESC LIMIT 10"
270
354
  ```
271
355
 
@@ -5,6 +5,9 @@ source "https://rubygems.org"
5
5
  gemspec path: ".."
6
6
 
7
7
  gem "rails", "~> 7.2"
8
+ # i18n >= 1.15 calls Fiber#[] (Ruby 3.2+) but declares no required_ruby_version,
9
+ # so it resolves on Ruby 3.1 and then crashes at load.
10
+ gem "i18n", "< 1.15"
8
11
  gem "gitlab-styles", "~> 12.0.1"
9
12
  gem "rake", "~> 13.0"
10
13
  gem "rspec", "~> 3.0"
@@ -30,6 +30,11 @@ module ClickHouse
30
30
  end
31
31
  end
32
32
 
33
+ # ClickHouse rejects a bare `UNION`; the mode must be explicit.
34
+ def visit_Arel_Nodes_Union(object, collector)
35
+ infix_value_with_paren(object, collector, " UNION DISTINCT ")
36
+ end
37
+
33
38
  def visit_ClickHouse_Client_ArelExtensions_Nodes_Final(object, collector)
34
39
  collector = visit(object.expr, collector)
35
40
  collector << " FINAL"
@@ -21,6 +21,12 @@ module ClickHouse
21
21
  # *logger*: object for receiving logger commands. Default `$stdout`
22
22
  # *log_proc*: any output (e.g. structure) to wrap around the query for every statement
23
23
  #
24
+ # *register_typecaster* (method): registers a custom typecaster for a column type,
25
+ # taking precedence over the built-in typecasters. Arguments:
26
+ # - type: column type as a String (exact match) or a Regexp
27
+ # - typecaster: a callable object receiving the column metadata Hash
28
+ # (e.g. { 'name' => 'title', 'type' => 'String' }) and the raw value
29
+ #
24
30
  # Example:
25
31
  #
26
32
  # ClickHouse::Client.configure do |c|
@@ -51,12 +57,17 @@ module ClickHouse
51
57
  # end
52
58
  #
53
59
  # c.json_parser = JSON
60
+ #
61
+ # c.register_typecaster('String', ->(meta, value) do
62
+ # meta['name'].match?(/\w+_json\z/) ? JSON.parse(value) : value
63
+ # end)
54
64
  # end
55
65
  attr_accessor :http_post_proc, :json_parser, :logger, :log_proc
56
- attr_reader :databases
66
+ attr_reader :databases, :typecasters
57
67
 
58
68
  def initialize
59
69
  @databases = {}
70
+ @typecasters = {}
60
71
  @http_post_proc = nil
61
72
  @json_parser = JSON
62
73
  @logger = ::Logger.new($stdout)
@@ -69,6 +80,12 @@ module ClickHouse
69
80
  @databases[name] = Database.new(**args)
70
81
  end
71
82
 
83
+ def register_typecaster(type, typecaster)
84
+ raise ConfigurationError, "The typecaster for '#{type}' is already registered" if @typecasters.key?(type)
85
+
86
+ @typecasters[type] = typecaster
87
+ end
88
+
72
89
  def validate!
73
90
  raise ConfigurationError, "The 'http_post_proc' option is not configured" unless @http_post_proc
74
91
  raise ConfigurationError, "The 'json_parser' option is not configured" unless @json_parser
@@ -19,9 +19,9 @@ module ClickHouse
19
19
  "IntervalMillisecond" => ->(value) { ActiveSupport::Duration.build(value.to_i / 1000.0) }
20
20
  }.freeze
21
21
 
22
- def self.format(result)
22
+ def self.format(result, custom_typecasters = {})
23
23
  column_typecasters = result['meta'].each_with_object({}) do |column, hash|
24
- hash[column['name']] = get_typecaster(column['type']) || DEFAULT
24
+ hash[column['name']] = build_typecaster(column, custom_typecasters)
25
25
  end
26
26
 
27
27
  result['data'].map do |row|
@@ -31,7 +31,14 @@ module ClickHouse
31
31
  end
32
32
  end
33
33
 
34
- def self.get_typecaster(column_type)
34
+ def self.build_typecaster(column, custom_typecasters)
35
+ custom_caster = get_typecaster(column['type'], custom_typecasters)
36
+ return ->(value) { custom_caster.call(column, value) } if custom_caster
37
+
38
+ get_typecaster(column['type']) || DEFAULT
39
+ end
40
+
41
+ def self.get_typecaster(column_type, typecasters = TYPE_CASTERS)
35
42
  return unless column_type
36
43
 
37
44
  inner_type = column_type
@@ -39,7 +46,7 @@ module ClickHouse
39
46
  .sub(/\ALowCardinality\((.+)\)\z/, '\1') # e.g. LowCardinality(String)
40
47
  .sub(/\ASimpleAggregateFunction\(.+,\s*(.+)\)\z/, '\1') # e.g. SimpleAggregateFunction(sum, UInt64)
41
48
 
42
- TYPE_CASTERS.each do |key, caster|
49
+ typecasters.each do |key, caster|
43
50
  return caster if key.is_a?(String) ? key == inner_type : key.match?(inner_type)
44
51
  end
45
52
 
@@ -51,14 +51,14 @@ module ClickHouse
51
51
  end
52
52
  end
53
53
 
54
- def to_redacted_sql(bind_index_manager = BindIndexManager.new)
54
+ def to_redacted_sql
55
55
  raw_query.gsub(PLACEHOLDER_REGEX) do |placeholder_in_query|
56
56
  value = placeholder_value(placeholder_in_query)
57
57
 
58
58
  if value.is_a?(QueryLike)
59
- value.to_redacted_sql(bind_index_manager)
59
+ value.to_redacted_sql
60
60
  else
61
- bind_index_manager.next_bind_str
61
+ '?'
62
62
  end
63
63
  end
64
64
  end
@@ -33,8 +33,11 @@ module ClickHouse
33
33
  delegate :[], to: :table
34
34
 
35
35
  def initialize(table, alias_name = nil)
36
- @table = if table.is_a?(self.class) # subquery
36
+ @table = case table
37
+ when self.class # subquery
37
38
  Arel::Nodes::TableAlias.new(table.to_arel, alias_name)
39
+ when Arel::Nodes::TableAlias
40
+ table
38
41
  else
39
42
  Arel::Table.new(table)
40
43
  end
@@ -144,6 +147,30 @@ module ClickHouse
144
147
  end
145
148
  end
146
149
 
150
+ # Inverts the direction of all ORDER BY clauses: ASC becomes DESC and
151
+ # vice versa.
152
+ #
153
+ # @example
154
+ # query.order(:id).reverse_order.to_sql
155
+ # # => "SELECT * FROM `test_table` ORDER BY `test_table`.`id` DESC"
156
+ #
157
+ # @raise [ArgumentError] if any ordering node has no explicit direction
158
+ # @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
159
+ def reverse_order
160
+ clone.tap do |new_instance|
161
+ new_instance.manager.ast.orders = new_instance.manager.ast.orders.map do |order|
162
+ case order
163
+ when Arel::Nodes::Ascending
164
+ Arel::Nodes::Descending.new(order.expr)
165
+ when Arel::Nodes::Descending
166
+ Arel::Nodes::Ascending.new(order.expr)
167
+ else
168
+ raise ArgumentError, "Cannot reverse ordering node without explicit direction: #{order.class.name}"
169
+ end
170
+ end
171
+ end
172
+ end
173
+
147
174
  def group(*columns)
148
175
  clone.tap do |new_instance|
149
176
  new_instance.manager.group(*columns)
@@ -182,6 +209,21 @@ module ClickHouse
182
209
  end
183
210
  end
184
211
 
212
+ # Returns a query that yields no rows.
213
+ #
214
+ # @example
215
+ # query.none.to_sql
216
+ # # => "SELECT * FROM `test_table` WHERE 1 = 0"
217
+ #
218
+ # @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
219
+ def none
220
+ clone.tap do |new_instance|
221
+ new_instance.manager.where(
222
+ Arel.sql('1 = 0')
223
+ )
224
+ end
225
+ end
226
+
185
227
  def from(subquery, alias_name)
186
228
  clone.tap do |new_instance|
187
229
  new_from = if subquery.is_a?(self.class)
@@ -198,6 +240,71 @@ module ClickHouse
198
240
  end
199
241
  end
200
242
 
243
+ # Wraps this query as a CTE node with the given name, so it can be passed
244
+ # to #with: `query_builder.with(sub_query.as_cte(:foo))`. The name is
245
+ # rendered as a SQL identifier, not a quoted value.
246
+ #
247
+ # The manager is cloned so later in-place mutations on this builder (e.g.
248
+ # #limit or #offset) do not alter the captured CTE body.
249
+ #
250
+ # @param name [String, Symbol] name the CTE is referenced by
251
+ # @return [Arel::Nodes::Cte]
252
+ def as_cte(name)
253
+ Arel::Nodes::Cte.new(Arel::Nodes::SqlLiteral.new(name.to_s), to_arel.clone)
254
+ end
255
+
256
+ # Attaches a CTE node, emitting `WITH name AS (...)` before the SELECT.
257
+ # The CTE can then be referenced by name in FROM and IN-subquery positions,
258
+ # which is useful when the same subquery is needed in more than one place.
259
+ #
260
+ # Build the node with #as_cte, for example
261
+ # `query_builder.with(sub_query.as_cte(:foo))`.
262
+ #
263
+ # Chained calls accumulate: `query_builder.with(a).with(b)` emits both
264
+ # CTEs. This differs from `Arel::SelectManager#with`, which replaces any
265
+ # previously set CTE and would keep only `b`.
266
+ #
267
+ # @param cte [Arel::Nodes::Cte] a CTE node from #as_cte
268
+ # @return [ClickHouse::Client::QueryBuilder]
269
+ def with(cte)
270
+ unless cte.is_a?(Arel::Nodes::Cte)
271
+ raise ArgumentError, "expected Arel::Nodes::Cte, got #{cte.class}. " \
272
+ 'Use #as_cte(name) to create one.'
273
+ end
274
+
275
+ clone.tap do |new_instance|
276
+ existing = new_instance.manager.ast.with
277
+ all_ctes = existing ? existing.children + [cte] : [cte]
278
+ validate_unique_cte_names!(all_ctes)
279
+ new_instance.manager.ast.with = Arel::Nodes::With.new(all_ctes)
280
+ end
281
+ end
282
+
283
+ # Combines this query with one or more other queries using UNION and
284
+ # returns a new builder that selects from the combined result as an
285
+ # aliased subquery, so further chaining (#where, #order, #limit, ...)
286
+ # applies to the outer SELECT.
287
+ #
288
+ # @param others [Array<ClickHouse::Client::QueryBuilder>] queries to combine with
289
+ # @param type [Symbol] :distinct or :all
290
+ # @param alias_name [String, Symbol] alias for the union subquery
291
+ # @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
292
+ def union(*others, type: :distinct, alias_name: 'union_subquery')
293
+ validate_union_type!(type)
294
+ raise ArgumentError, 'at least one query to union with is required' if others.empty?
295
+
296
+ others.each do |other|
297
+ raise ArgumentError, "expected #{self.class}, got #{other.class}" unless other.is_a?(self.class)
298
+ end
299
+
300
+ node_class = type == :all ? Arel::Nodes::UnionAll : Arel::Nodes::Union
301
+ union_node = others.reduce(to_arel.clone.ast) do |left, other|
302
+ node_class.new(left, other.to_arel.clone.ast)
303
+ end
304
+
305
+ self.class.new(Arel::Nodes::TableAlias.new(union_node, alias_name.to_s))
306
+ end
307
+
201
308
  # Adds a JOIN clause. Pass `type: :outer` for `LEFT OUTER JOIN`.
202
309
  # To join a subquery, pre-alias it via `QueryBuilder.new(sub, 'x').table`
203
310
  # or `sub.to_arel.as('x')` and pass that.
@@ -383,9 +490,8 @@ module ClickHouse
383
490
  visitor.accept(manager.ast, Arel::Collectors::SQLString.new).value
384
491
  end
385
492
 
386
- def to_redacted_sql(bind_index_manager = ClickHouse::Client::BindIndexManager.new)
387
- visitor = ClickHouse::Client::ToRedactedSqlVisitor.new(ClickHouse::Client::ArelEngine.new,
388
- bind_manager: bind_index_manager)
493
+ def to_redacted_sql
494
+ visitor = ClickHouse::Client::ToRedactedSqlVisitor.new(ClickHouse::Client::ArelEngine.new)
389
495
  visitor.accept(manager.ast, Arel::Collectors::SQLString.new).value
390
496
  end
391
497
 
@@ -459,11 +565,26 @@ module ClickHouse
459
565
  raise ArgumentError, "Invalid order direction '#{direction}'. Must be :asc or :desc"
460
566
  end
461
567
 
568
+ def validate_union_type!(type)
569
+ return if %i[distinct all].include?(type)
570
+
571
+ raise ArgumentError, "Invalid union type '#{type}'. Must be :distinct or :all"
572
+ end
573
+
462
574
  def validate_join_type!(type)
463
575
  return if %i[inner outer].include?(type)
464
576
 
465
577
  raise ArgumentError, "Invalid join type '#{type}'. Must be :inner or :outer"
466
578
  end
579
+
580
+ def validate_unique_cte_names!(ctes)
581
+ names = ctes.map { |cte| cte.name.to_s }
582
+ duplicates = names.tally.select { |_, count| count > 1 }.keys
583
+
584
+ return if duplicates.empty?
585
+
586
+ raise ArgumentError, "duplicate CTE name(s): #{duplicates.join(', ')}"
587
+ end
467
588
  end
468
589
  end
469
590
  end
@@ -11,7 +11,7 @@ module ClickHouse
11
11
  # Redacted version of the SQL query generated by the to_sql method where the
12
12
  # placeholders are stripped. These queries are meant to be exported to external
13
13
  # log aggregation systems.
14
- def to_redacted_sql(bind_index_manager = BindIndexManager.new)
14
+ def to_redacted_sql
15
15
  raise NotImplementedError
16
16
  end
17
17
 
@@ -8,15 +8,8 @@ module ClickHouse
8
8
  # query_builder = ClickHouse::QueryBuilder.new('users').where(name: 'John Doe')
9
9
  # redacted_query = query_builder.to_redacted_sql
10
10
  # # The redacted_query will contain the SQL query with values replaced by placeholders.
11
- # output: "SELECT * FROM \"users\" WHERE \"users\".\"name\" = $1"
11
+ # output: "SELECT * FROM \"users\" WHERE \"users\".\"name\" = ?"
12
12
  class ToRedactedSqlVisitor < ArelVisitor
13
- attr_reader :bind_manager
14
-
15
- def initialize(*args, bind_manager: ClickHouse::Client::BindIndexManager.new)
16
- @bind_manager = bind_manager
17
- super(*args)
18
- end
19
-
20
13
  private
21
14
 
22
15
  def redaction_enabled?
@@ -80,11 +73,9 @@ module ClickHouse
80
73
 
81
74
  case redacted_o.name
82
75
  when 'startsWith'
83
- redacted_o.expressions[1] = Arel.sql(bind_manager.next_bind_str)
76
+ redacted_o.expressions[1] = bind_param
84
77
  else
85
- redacted_o.expressions = redacted_o.expressions.map do
86
- Arel.sql(bind_manager.next_bind_str)
87
- end
78
+ redacted_o.expressions = redacted_o.expressions.map { bind_param }
88
79
  end
89
80
 
90
81
  super(redacted_o, collector)
@@ -100,17 +91,19 @@ module ClickHouse
100
91
  cloned_o = o.clone
101
92
 
102
93
  redacted_right = if o.right.is_a?(Array)
103
- Array.new(o.right.size) do
104
- Arel.sql(bind_manager.next_bind_str)
105
- end
94
+ Array.new(o.right.size) { bind_param }
106
95
  else
107
- Arel.sql(bind_manager.next_bind_str)
96
+ bind_param
108
97
  end
109
98
 
110
99
  cloned_o.right = redacted_right
111
100
  cloned_o
112
101
  end
113
102
 
103
+ def bind_param
104
+ Arel::Nodes::BindParam.new('?')
105
+ end
106
+
114
107
  # rubocop:enable Naming/MethodName
115
108
  # rubocop:enable Naming/MethodParameterName
116
109
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ClickHouse
4
4
  module Client
5
- VERSION = "0.10.0"
5
+ VERSION = "0.12.0"
6
6
  end
7
7
  end
@@ -10,7 +10,6 @@ require 'active_support/core_ext/object/blank'
10
10
  require_relative "client/version"
11
11
  require_relative "client/database"
12
12
  require_relative "client/configuration"
13
- require_relative "client/bind_index_manager"
14
13
  require_relative "client/quoting"
15
14
  require_relative "client/arel_engine"
16
15
  require_relative "client/query_like"
@@ -50,7 +49,7 @@ module ClickHouse
50
49
 
51
50
  instrument[:statistics] = parsed_response['statistics']&.symbolize_keys
52
51
 
53
- Formatter.format(parsed_response)
52
+ Formatter.format(parsed_response, configuration.typecasters)
54
53
  end
55
54
  end
56
55
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: click_house-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - group::optimize
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-04 00:00:00.000000000 Z
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -187,7 +187,6 @@ files:
187
187
  - lib/click_house/client/arel_engine.rb
188
188
  - lib/click_house/client/arel_extensions/nodes/final.rb
189
189
  - lib/click_house/client/arel_visitor.rb
190
- - lib/click_house/client/bind_index_manager.rb
191
190
  - lib/click_house/client/configuration.rb
192
191
  - lib/click_house/client/database.rb
193
192
  - lib/click_house/client/formatter.rb
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ClickHouse
4
- module Client
5
- class BindIndexManager
6
- def initialize(start_index = 1)
7
- @current_index = start_index
8
- end
9
-
10
- def next_bind_str
11
- bind_str = "$#{@current_index}"
12
- @current_index += 1
13
- bind_str
14
- end
15
- end
16
- end
17
- end