click_house-client 0.8.3 → 0.10.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/.gitignore +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +31 -1
- data/lib/click_house/client/arel_extensions/nodes/final.rb +14 -0
- data/lib/click_house/client/arel_visitor.rb +5 -0
- data/lib/click_house/client/formatter.rb +28 -16
- data/lib/click_house/client/query_builder.rb +81 -11
- data/lib/click_house/client/quoting.rb +1 -0
- data/lib/click_house/client/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bef646c7c07c30559265378e9fec3f14f5e41aeadd86058120a31d532d77d10a
|
|
4
|
+
data.tar.gz: e969763ad335eda82d36f3d949207cfcb95ab238646ff0b3e4ec18e5a17bd445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f7883a21fba0ab5534c85ca43686a3490234252c2277e200a244a3bead56fa3a31d7597c8b23deecb0086b5ad3a90dde67c8425f8ce257c289cb29abb8b29ad
|
|
7
|
+
data.tar.gz: 175aaeaefe0f02f0ba1cd9e3828d9d4bb876c450f626765b78568297c0d35d7e2eb7de00f900ea88db84f7240fbb32790fededbd258dcad4afb4d9291bb16f7c
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.rspec_status
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
click_house-client (0.
|
|
4
|
+
click_house-client (0.10.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.
|
|
139
|
+
click_house-client (0.10.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
|
@@ -182,9 +182,28 @@ query
|
|
|
182
182
|
# => "SELECT * FROM `users` WHERE `users`.`active` = 'true' GROUP BY `users`.`department` HAVING `users`.`avg_salary` > 50000"
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
### FINAL Modifier
|
|
186
|
+
|
|
187
|
+
ClickHouse's `FINAL` modifier forces merging of rows during query time for tables in the MergeTree family. Apply it via `.final`:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
query.final.to_sql
|
|
191
|
+
# => "SELECT * FROM `users` FINAL"
|
|
192
|
+
|
|
193
|
+
query.final.where(active: true).to_sql
|
|
194
|
+
# => "SELECT * FROM `users` FINAL WHERE `users`.`active` = 'true'"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> ⚠️ **Warning:** Using `FINAL` in production code can cause excessive I/O and affect ClickHouse availability. Prefer using it only in test environments or behind a feature flag.
|
|
198
|
+
|
|
199
|
+
`FINAL` is currently applied only to the main `FROM` table. When joining, joined tables are not marked `FINAL`. Calling `.final` multiple times is idempotent.
|
|
200
|
+
|
|
185
201
|
### Working with JOINs
|
|
186
202
|
|
|
187
|
-
|
|
203
|
+
`#joins` supports `INNER JOIN` (default) and `LEFT OUTER JOIN` via
|
|
204
|
+
`type: :outer`. The join source can be a table name, an `Arel::Table`, or a
|
|
205
|
+
pre-aliased subquery (`QueryBuilder.new(sub, 'alias').table` or
|
|
206
|
+
`sub.to_arel.as('alias')`).
|
|
188
207
|
|
|
189
208
|
```ruby
|
|
190
209
|
# Join with conditions on joined table
|
|
@@ -201,6 +220,17 @@ query
|
|
|
201
220
|
.having(orders: { total: [100, 200, 300] })
|
|
202
221
|
.to_sql
|
|
203
222
|
# => "SELECT * FROM `users` INNER JOIN `orders` ON `users`.`id` = `orders`.`user_id` GROUP BY `users`.`department` HAVING `orders`.`total` IN (100, 200, 300)"
|
|
223
|
+
|
|
224
|
+
# LEFT OUTER JOIN against a pre-aliased subquery
|
|
225
|
+
orders_sub = ClickHouse::Client::QueryBuilder.new(
|
|
226
|
+
ClickHouse::Client::QueryBuilder.new('orders').select(:id, :user_id),
|
|
227
|
+
'o'
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
query
|
|
231
|
+
.joins(orders_sub.table, { id: :user_id }, type: :outer)
|
|
232
|
+
.to_sql
|
|
233
|
+
# => "SELECT * FROM `users` LEFT OUTER JOIN (SELECT `orders`.`id`, `orders`.`user_id` FROM `orders`) `o` ON `users`.`id` = `o`.`user_id`"
|
|
204
234
|
```
|
|
205
235
|
|
|
206
236
|
### Complete Example
|
|
@@ -5,34 +5,46 @@ module ClickHouse
|
|
|
5
5
|
class Formatter
|
|
6
6
|
DEFAULT = ->(value) { value }
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
TYPE_CASTERS = {
|
|
9
|
+
# UInt8/16/32/64, Int8/16/32/64
|
|
10
|
+
/\AU?Int\d+\z/ => ->(value) { Integer(value) },
|
|
11
|
+
# Float8/16/32/64
|
|
12
|
+
/\AFloat\d+\z/ => ->(value) { Float(value) },
|
|
13
|
+
# Date, Date32
|
|
14
|
+
/\ADate\d*\z/ => ->(value) { Date.parse(value) },
|
|
15
|
+
# DateTime64(0/3/6/9, 'UTC')
|
|
16
|
+
/\ADateTime64\(\d+, 'UTC'\)\z/ => ->(value) { ActiveSupport::TimeZone['UTC'].parse(value) },
|
|
17
|
+
"DateTime('UTC')" => ->(value) { ActiveSupport::TimeZone['UTC'].parse(value) },
|
|
14
18
|
"IntervalSecond" => ->(value) { ActiveSupport::Duration.build(value.to_i) },
|
|
15
19
|
"IntervalMillisecond" => ->(value) { ActiveSupport::Duration.build(value.to_i / 1000.0) }
|
|
16
20
|
}.freeze
|
|
17
21
|
|
|
18
|
-
TYPE_CASTERS = BASIC_TYPE_CASTERS.merge(
|
|
19
|
-
BASIC_TYPE_CASTERS.transform_keys { |type| "Nullable(#{type})" }
|
|
20
|
-
.transform_values { |caster| ->(value) { value.nil? ? nil : caster.call(value) } }
|
|
21
|
-
)
|
|
22
|
-
|
|
23
22
|
def self.format(result)
|
|
24
|
-
|
|
25
|
-
hash[column['name']] = column['type']
|
|
23
|
+
column_typecasters = result['meta'].each_with_object({}) do |column, hash|
|
|
24
|
+
hash[column['name']] = get_typecaster(column['type']) || DEFAULT
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
result['data'].map do |row|
|
|
29
28
|
row.each_with_object({}) do |(column, value), casted_row|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
casted_row[column] = caster.call(value)
|
|
29
|
+
casted_row[column] = value.nil? ? value : column_typecasters[column].call(value)
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
32
|
end
|
|
33
|
+
|
|
34
|
+
def self.get_typecaster(column_type)
|
|
35
|
+
return unless column_type
|
|
36
|
+
|
|
37
|
+
inner_type = column_type
|
|
38
|
+
.sub(/\ANullable\((.+)\)\z/, '\1') # e.g Nullable(String)
|
|
39
|
+
.sub(/\ALowCardinality\((.+)\)\z/, '\1') # e.g. LowCardinality(String)
|
|
40
|
+
.sub(/\ASimpleAggregateFunction\(.+,\s*(.+)\)\z/, '\1') # e.g. SimpleAggregateFunction(sum, UInt64)
|
|
41
|
+
|
|
42
|
+
TYPE_CASTERS.each do |key, caster|
|
|
43
|
+
return caster if key.is_a?(String) ? key == inner_type : key.match?(inner_type)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
36
48
|
end
|
|
37
49
|
end
|
|
38
50
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_record'
|
|
4
|
+
require_relative 'arel_extensions/nodes/final'
|
|
4
5
|
|
|
5
6
|
module ClickHouse
|
|
6
7
|
module Client
|
|
@@ -29,6 +30,8 @@ module ClickHouse
|
|
|
29
30
|
Arel::Nodes::As
|
|
30
31
|
].freeze
|
|
31
32
|
|
|
33
|
+
delegate :[], to: :table
|
|
34
|
+
|
|
32
35
|
def initialize(table, alias_name = nil)
|
|
33
36
|
@table = if table.is_a?(self.class) # subquery
|
|
34
37
|
Arel::Nodes::TableAlias.new(table.to_arel, alias_name)
|
|
@@ -68,6 +71,15 @@ module ClickHouse
|
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
# Evaluates given block in scope of current builder. Example:
|
|
75
|
+
# query = ClickHouse::Client::QueryBuilder.new('test_table').build do
|
|
76
|
+
# select(named_func('argMax', [table[:id], table[:timestamp]]).as('max_id')).limit(10)
|
|
77
|
+
# end
|
|
78
|
+
# @return [ClickHouse::QueryBuilder] New instance of query builder.
|
|
79
|
+
def build(&)
|
|
80
|
+
instance_eval(&)
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
# The `having` method applies constraints to the HAVING clause, similar to how
|
|
72
84
|
# `where` applies constraints to the WHERE clause. It supports the same constraint types.
|
|
73
85
|
# Correct usage:
|
|
@@ -148,44 +160,96 @@ module ClickHouse
|
|
|
148
160
|
self
|
|
149
161
|
end
|
|
150
162
|
|
|
163
|
+
# Applies the ClickHouse `FINAL` modifier to the main FROM table.
|
|
164
|
+
# See https://clickhouse.com/docs/en/sql-reference/statements/select/from#final-modifier
|
|
165
|
+
#
|
|
166
|
+
# @example
|
|
167
|
+
# query.final.to_sql
|
|
168
|
+
# # => "SELECT * FROM `test_table` FINAL"
|
|
169
|
+
#
|
|
170
|
+
# Note: `FINAL` only makes sense on a ClickHouse MergeTree-family table.
|
|
171
|
+
#
|
|
172
|
+
# WARNING: Using `FINAL` in production code can cause excessive I/O and
|
|
173
|
+
# affect ClickHouse availability. Prefer using it only in test environments
|
|
174
|
+
# or behind a feature flag.
|
|
175
|
+
#
|
|
176
|
+
# @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
|
|
177
|
+
def final
|
|
178
|
+
clone.tap do |new_instance|
|
|
179
|
+
source = new_instance.manager.source.left
|
|
180
|
+
wrapped = source.is_a?(ArelExtensions::Nodes::Final) ? source : ArelExtensions::Nodes::Final.new(source)
|
|
181
|
+
new_instance.manager.from(wrapped)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
151
185
|
def from(subquery, alias_name)
|
|
152
186
|
clone.tap do |new_instance|
|
|
153
|
-
if subquery.is_a?(self.class)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
187
|
+
new_from = if subquery.is_a?(self.class)
|
|
188
|
+
subquery.to_arel.as(alias_name)
|
|
189
|
+
else
|
|
190
|
+
Arel::Nodes::TableAlias.new(subquery, alias_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if new_instance.manager.source.left.is_a?(ArelExtensions::Nodes::Final)
|
|
194
|
+
new_from = ArelExtensions::Nodes::Final.new(new_from)
|
|
157
195
|
end
|
|
196
|
+
|
|
197
|
+
new_instance.manager.from(new_from)
|
|
158
198
|
end
|
|
159
199
|
end
|
|
160
200
|
|
|
161
|
-
|
|
201
|
+
# Adds a JOIN clause. Pass `type: :outer` for `LEFT OUTER JOIN`.
|
|
202
|
+
# To join a subquery, pre-alias it via `QueryBuilder.new(sub, 'x').table`
|
|
203
|
+
# or `sub.to_arel.as('x')` and pass that.
|
|
204
|
+
# @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
|
|
205
|
+
def joins(source, constraint = nil, type: :inner)
|
|
206
|
+
validate_join_type!(type)
|
|
207
|
+
|
|
162
208
|
clone.tap do |new_instance|
|
|
163
|
-
|
|
209
|
+
join_target = case source
|
|
210
|
+
when Arel::Table, Arel::Nodes::TableAlias then source
|
|
211
|
+
else Arel::Table.new(source)
|
|
212
|
+
end
|
|
213
|
+
join_class = type == :outer ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
|
164
214
|
|
|
165
215
|
join_condition = case constraint
|
|
166
216
|
when Hash
|
|
167
217
|
# Handle hash based constraints like { table1.id: table2.ref_id } or {id: :ref_id}
|
|
168
218
|
constraint_conditions = constraint.map do |left, right|
|
|
169
219
|
left_field = left.is_a?(Arel::Attributes::Attribute) ? left : new_instance.table[left]
|
|
170
|
-
right_field = right.is_a?(Arel::Attributes::Attribute) ? right :
|
|
220
|
+
right_field = right.is_a?(Arel::Attributes::Attribute) ? right : join_target[right]
|
|
171
221
|
left_field.eq(right_field)
|
|
172
222
|
end
|
|
173
223
|
|
|
174
224
|
constraint_conditions.reduce(&:and)
|
|
175
225
|
when Proc
|
|
176
|
-
constraint.call(new_instance.table,
|
|
177
|
-
when Arel::Nodes::Node
|
|
226
|
+
constraint.call(new_instance.table, join_target)
|
|
227
|
+
when Arel::Nodes::Node, Arel::Nodes::SqlLiteral
|
|
178
228
|
constraint
|
|
179
229
|
end
|
|
180
230
|
|
|
181
231
|
if join_condition
|
|
182
|
-
new_instance.manager.join(
|
|
232
|
+
new_instance.manager.join(join_target, join_class).on(join_condition)
|
|
183
233
|
else
|
|
184
|
-
new_instance.manager.join(
|
|
234
|
+
new_instance.manager.join(join_target, join_class)
|
|
185
235
|
end
|
|
186
236
|
end
|
|
187
237
|
end
|
|
188
238
|
|
|
239
|
+
# Shortcut for `Arel::Nodes::NamedFunction.new`
|
|
240
|
+
# @return [Arel::Nodes::NamedFunction]
|
|
241
|
+
def named_func(*args)
|
|
242
|
+
Arel::Nodes::NamedFunction.new(*args)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Shortcut for `Arel::Nodes.build_quoted`
|
|
246
|
+
# @return [Arel::Nodes::Node]
|
|
247
|
+
def quote(...)
|
|
248
|
+
Arel::Nodes.build_quoted(...)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
alias_method :func, :named_func
|
|
252
|
+
|
|
189
253
|
# Aggregation helper methods
|
|
190
254
|
|
|
191
255
|
# Creates an AVG aggregate function node
|
|
@@ -394,6 +458,12 @@ module ClickHouse
|
|
|
394
458
|
|
|
395
459
|
raise ArgumentError, "Invalid order direction '#{direction}'. Must be :asc or :desc"
|
|
396
460
|
end
|
|
461
|
+
|
|
462
|
+
def validate_join_type!(type)
|
|
463
|
+
return if %i[inner outer].include?(type)
|
|
464
|
+
|
|
465
|
+
raise ArgumentError, "Invalid join type '#{type}'. Must be :inner or :outer"
|
|
466
|
+
end
|
|
397
467
|
end
|
|
398
468
|
end
|
|
399
469
|
end
|
|
@@ -9,6 +9,7 @@ module ClickHouse
|
|
|
9
9
|
when Numeric then value.to_s
|
|
10
10
|
when String, Symbol then "'#{value.to_s.gsub('\\', '\&\&').gsub("'", "''")}'"
|
|
11
11
|
when Array then "[#{value.map { |v| quote(v) }.join(',')}]"
|
|
12
|
+
when Time then value.utc.to_f
|
|
12
13
|
when nil then "NULL"
|
|
13
14
|
else quote_str(value.to_s)
|
|
14
15
|
end
|
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.
|
|
4
|
+
version: 0.10.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:
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -170,6 +170,7 @@ executables: []
|
|
|
170
170
|
extensions: []
|
|
171
171
|
extra_rdoc_files: []
|
|
172
172
|
files:
|
|
173
|
+
- ".gitignore"
|
|
173
174
|
- ".gitlab-ci.yml"
|
|
174
175
|
- ".rspec"
|
|
175
176
|
- ".rubocop.yml"
|
|
@@ -184,6 +185,7 @@ files:
|
|
|
184
185
|
- gemfiles/Gemfile-rails-8.0
|
|
185
186
|
- lib/click_house/client.rb
|
|
186
187
|
- lib/click_house/client/arel_engine.rb
|
|
188
|
+
- lib/click_house/client/arel_extensions/nodes/final.rb
|
|
187
189
|
- lib/click_house/client/arel_visitor.rb
|
|
188
190
|
- lib/click_house/client/bind_index_manager.rb
|
|
189
191
|
- lib/click_house/client/configuration.rb
|