click_house-client 0.9.0 → 0.11.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/Gemfile.lock +2 -2
- data/README.md +42 -1
- data/gemfiles/Gemfile-rails-7.2 +3 -0
- data/lib/click_house/client/query_builder.rb +72 -7
- data/lib/click_house/client/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20e1cebe2da3ec70c0a9ab7be6e5e53d2f2462b4787fd7e7389adfdbe4c64ea9
|
|
4
|
+
data.tar.gz: 0bdebe100594f4025f53a5c28723ef776d10ff327283166f70c9990a3323bb62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58e7ed6985feda6a89dd33a1eac938509d3515016eea6c92ea38621c327a94d6e997dd166d67b3b7a2ddd1350edee0e3997055301159c01e8b0e3cac75531707
|
|
7
|
+
data.tar.gz: fd25cb3026f5be1d693181cbab8c13915abdad23ac0acd31e4d098b4477c35d6ef99e010e3e4c1477eb00b729bbdaecc5d4a2b40f36d1a35d147f4cdd6f1402f
|
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.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.
|
|
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
|
@@ -200,7 +200,10 @@ query.final.where(active: true).to_sql
|
|
|
200
200
|
|
|
201
201
|
### Working with JOINs
|
|
202
202
|
|
|
203
|
-
|
|
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')`).
|
|
204
207
|
|
|
205
208
|
```ruby
|
|
206
209
|
# Join with conditions on joined table
|
|
@@ -217,8 +220,46 @@ query
|
|
|
217
220
|
.having(orders: { total: [100, 200, 300] })
|
|
218
221
|
.to_sql
|
|
219
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`"
|
|
220
234
|
```
|
|
221
235
|
|
|
236
|
+
### Common Table Expressions (CTEs)
|
|
237
|
+
|
|
238
|
+
Use `#as_cte(name)` to wrap a query as a named CTE node, then attach it to a
|
|
239
|
+
main query with `#with(cte)`. The CTE can then be referenced by name in `FROM`
|
|
240
|
+
and IN-subquery positions, which is useful when the same subquery is needed in
|
|
241
|
+
more than one place.
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
inner = ClickHouse::Client::QueryBuilder.new('builds').select(:id, :stage_id)
|
|
245
|
+
query_builder = ClickHouse::Client::QueryBuilder.new('finished_builds').select(:id)
|
|
246
|
+
|
|
247
|
+
query_builder.with(inner.as_cte(:finished_builds)).to_sql
|
|
248
|
+
# => "WITH finished_builds AS (SELECT `builds`.`id`, `builds`.`stage_id` FROM `builds`) SELECT `finished_builds`.`id` FROM `finished_builds`"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Chained calls accumulate, unlike `Arel::SelectManager#with` which would keep
|
|
252
|
+
only the last CTE:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
query_builder.with(inner.as_cte(:a)).with(inner.as_cte(:b)).to_sql
|
|
256
|
+
# => "WITH a AS (...), b AS (...) SELECT ..."
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`#with` returns a new `QueryBuilder` (immutable), requires an `Arel::Nodes::Cte`
|
|
260
|
+
(build it via `#as_cte`), and raises if the same CTE name is declared more than
|
|
261
|
+
once.
|
|
262
|
+
|
|
222
263
|
### Complete Example
|
|
223
264
|
|
|
224
265
|
Here's a comprehensive example combining multiple QueryBuilder features:
|
data/gemfiles/Gemfile-rails-7.2
CHANGED
|
@@ -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"
|
|
@@ -198,30 +198,80 @@ module ClickHouse
|
|
|
198
198
|
end
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
# Wraps this query as a CTE node with the given name, so it can be passed
|
|
202
|
+
# to #with: `query_builder.with(sub_query.as_cte(:foo))`. The name is
|
|
203
|
+
# rendered as a SQL identifier, not a quoted value.
|
|
204
|
+
#
|
|
205
|
+
# The manager is cloned so later in-place mutations on this builder (e.g.
|
|
206
|
+
# #limit or #offset) do not alter the captured CTE body.
|
|
207
|
+
#
|
|
208
|
+
# @param name [String, Symbol] name the CTE is referenced by
|
|
209
|
+
# @return [Arel::Nodes::Cte]
|
|
210
|
+
def as_cte(name)
|
|
211
|
+
Arel::Nodes::Cte.new(Arel::Nodes::SqlLiteral.new(name.to_s), to_arel.clone)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Attaches a CTE node, emitting `WITH name AS (...)` before the SELECT.
|
|
215
|
+
# The CTE can then be referenced by name in FROM and IN-subquery positions,
|
|
216
|
+
# which is useful when the same subquery is needed in more than one place.
|
|
217
|
+
#
|
|
218
|
+
# Build the node with #as_cte, for example
|
|
219
|
+
# `query_builder.with(sub_query.as_cte(:foo))`.
|
|
220
|
+
#
|
|
221
|
+
# Chained calls accumulate: `query_builder.with(a).with(b)` emits both
|
|
222
|
+
# CTEs. This differs from `Arel::SelectManager#with`, which replaces any
|
|
223
|
+
# previously set CTE and would keep only `b`.
|
|
224
|
+
#
|
|
225
|
+
# @param cte [Arel::Nodes::Cte] a CTE node from #as_cte
|
|
226
|
+
# @return [ClickHouse::Client::QueryBuilder]
|
|
227
|
+
def with(cte)
|
|
228
|
+
unless cte.is_a?(Arel::Nodes::Cte)
|
|
229
|
+
raise ArgumentError, "expected Arel::Nodes::Cte, got #{cte.class}. " \
|
|
230
|
+
'Use #as_cte(name) to create one.'
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
clone.tap do |new_instance|
|
|
234
|
+
existing = new_instance.manager.ast.with
|
|
235
|
+
all_ctes = existing ? existing.children + [cte] : [cte]
|
|
236
|
+
validate_unique_cte_names!(all_ctes)
|
|
237
|
+
new_instance.manager.ast.with = Arel::Nodes::With.new(all_ctes)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Adds a JOIN clause. Pass `type: :outer` for `LEFT OUTER JOIN`.
|
|
242
|
+
# To join a subquery, pre-alias it via `QueryBuilder.new(sub, 'x').table`
|
|
243
|
+
# or `sub.to_arel.as('x')` and pass that.
|
|
244
|
+
# @return [ClickHouse::Client::QueryBuilder] New instance of query builder.
|
|
245
|
+
def joins(source, constraint = nil, type: :inner)
|
|
246
|
+
validate_join_type!(type)
|
|
247
|
+
|
|
202
248
|
clone.tap do |new_instance|
|
|
203
|
-
|
|
249
|
+
join_target = case source
|
|
250
|
+
when Arel::Table, Arel::Nodes::TableAlias then source
|
|
251
|
+
else Arel::Table.new(source)
|
|
252
|
+
end
|
|
253
|
+
join_class = type == :outer ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
|
204
254
|
|
|
205
255
|
join_condition = case constraint
|
|
206
256
|
when Hash
|
|
207
257
|
# Handle hash based constraints like { table1.id: table2.ref_id } or {id: :ref_id}
|
|
208
258
|
constraint_conditions = constraint.map do |left, right|
|
|
209
259
|
left_field = left.is_a?(Arel::Attributes::Attribute) ? left : new_instance.table[left]
|
|
210
|
-
right_field = right.is_a?(Arel::Attributes::Attribute) ? right :
|
|
260
|
+
right_field = right.is_a?(Arel::Attributes::Attribute) ? right : join_target[right]
|
|
211
261
|
left_field.eq(right_field)
|
|
212
262
|
end
|
|
213
263
|
|
|
214
264
|
constraint_conditions.reduce(&:and)
|
|
215
265
|
when Proc
|
|
216
|
-
constraint.call(new_instance.table,
|
|
217
|
-
when Arel::Nodes::Node
|
|
266
|
+
constraint.call(new_instance.table, join_target)
|
|
267
|
+
when Arel::Nodes::Node, Arel::Nodes::SqlLiteral
|
|
218
268
|
constraint
|
|
219
269
|
end
|
|
220
270
|
|
|
221
271
|
if join_condition
|
|
222
|
-
new_instance.manager.join(
|
|
272
|
+
new_instance.manager.join(join_target, join_class).on(join_condition)
|
|
223
273
|
else
|
|
224
|
-
new_instance.manager.join(
|
|
274
|
+
new_instance.manager.join(join_target, join_class)
|
|
225
275
|
end
|
|
226
276
|
end
|
|
227
277
|
end
|
|
@@ -448,6 +498,21 @@ module ClickHouse
|
|
|
448
498
|
|
|
449
499
|
raise ArgumentError, "Invalid order direction '#{direction}'. Must be :asc or :desc"
|
|
450
500
|
end
|
|
501
|
+
|
|
502
|
+
def validate_join_type!(type)
|
|
503
|
+
return if %i[inner outer].include?(type)
|
|
504
|
+
|
|
505
|
+
raise ArgumentError, "Invalid join type '#{type}'. Must be :inner or :outer"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def validate_unique_cte_names!(ctes)
|
|
509
|
+
names = ctes.map { |cte| cte.name.to_s }
|
|
510
|
+
duplicates = names.tally.select { |_, count| count > 1 }.keys
|
|
511
|
+
|
|
512
|
+
return if duplicates.empty?
|
|
513
|
+
|
|
514
|
+
raise ArgumentError, "duplicate CTE name(s): #{duplicates.join(', ')}"
|
|
515
|
+
end
|
|
451
516
|
end
|
|
452
517
|
end
|
|
453
518
|
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.11.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-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|