click_house-client 0.4.1 → 0.5.1
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 +13 -5
- data/lib/click_house/client/formatter.rb +3 -0
- data/lib/click_house/client/query_builder.rb +70 -39
- data/lib/click_house/client/redactor.rb +56 -51
- 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: 55b79507a43d0c64c35ed2a024c4314e6d8d1f4daf227aee3db84947d34e2a2a
|
4
|
+
data.tar.gz: 6673504ba4ffe2b790064529600e3e66e5e7c4d687cc82f15fb9237cd5661c94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41b0de769672003cd9ab1e8d755419191ba53f47786f555f12c018c8a41cc101f0196f48ea7f2f7ad1f0f0390eb1be04e14bb7d0c0f5d351b8b712b1b7dcfae3
|
7
|
+
data.tar.gz: 3cd5e7948eb80aedf7f75b4470d067b4ef7e1a70b987b349786700d5aa07b981439d8baa75fc399def7a4f2ed80e8ba47f0c8779cf12fc06771626e2f0ebc605
|
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.5.1)
|
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.5.1)
|
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
@@ -22,10 +22,13 @@ ClickHouse::Client.configure do |config|
|
|
22
22
|
|
23
23
|
# Use any HTTP client to build the POST request, here we use Net::HTTP
|
24
24
|
config.http_post_proc = ->(url, headers, body) do
|
25
|
-
|
26
|
-
|
25
|
+
uri = URI.parse(url)
|
26
|
+
|
27
|
+
unless body.is_a?(IO)
|
28
|
+
# Append placeholders to URI's query
|
29
|
+
uri.query = [uri.query, URI.encode_www_form(body.except("query"))].compact.join('&')
|
30
|
+
end
|
27
31
|
|
28
|
-
uri = URI.parse("#{url}&#{params}")
|
29
32
|
request = Net::HTTP::Post.new(uri)
|
30
33
|
|
31
34
|
headers.each do |header, value|
|
@@ -33,9 +36,14 @@ ClickHouse::Client.configure do |config|
|
|
33
36
|
end
|
34
37
|
|
35
38
|
request['Content-type'] = 'application/x-www-form-urlencoded'
|
36
|
-
request.body = body["query"]
|
37
39
|
|
38
|
-
|
40
|
+
if body.is_a?(IO)
|
41
|
+
request.body_stream = body
|
42
|
+
else
|
43
|
+
request.body = body['query']
|
44
|
+
end
|
45
|
+
|
46
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
39
47
|
http.request(request)
|
40
48
|
end
|
41
49
|
|
@@ -6,6 +6,9 @@ module ClickHouse
|
|
6
6
|
DEFAULT = ->(value) { value }
|
7
7
|
|
8
8
|
BASIC_TYPE_CASTERS = {
|
9
|
+
'Int32' => ->(value) { Integer(value) },
|
10
|
+
'UInt32' => ->(value) { Integer(value) },
|
11
|
+
'Int64' => ->(value) { Integer(value) },
|
9
12
|
'UInt64' => ->(value) { Integer(value) },
|
10
13
|
"DateTime64(6, 'UTC')" => ->(value) { ActiveSupport::TimeZone['UTC'].parse(value) },
|
11
14
|
"IntervalSecond" => ->(value) { ActiveSupport::Duration.build(value.to_i) },
|
@@ -6,7 +6,7 @@ module ClickHouse
|
|
6
6
|
module Client
|
7
7
|
class QueryBuilder < QueryLike
|
8
8
|
attr_reader :table
|
9
|
-
attr_accessor :
|
9
|
+
attr_accessor :manager
|
10
10
|
|
11
11
|
VALID_NODES = [
|
12
12
|
Arel::Nodes::In,
|
@@ -27,38 +27,39 @@ module ClickHouse
|
|
27
27
|
def initialize(table_name)
|
28
28
|
@table = Arel::Table.new(table_name)
|
29
29
|
@manager = Arel::SelectManager.new(Arel::Table.engine).from(@table).project(Arel.star)
|
30
|
-
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize_copy(other)
|
33
|
+
super
|
34
|
+
|
35
|
+
@manager = other.manager.clone
|
31
36
|
end
|
32
37
|
|
33
38
|
# The `where` method currently only supports IN and equal to queries along
|
34
39
|
# with above listed VALID_NODES.
|
35
40
|
# For example, using a range (start_date..end_date) will result in incorrect SQL.
|
36
|
-
# If you need to query a range, use greater than and less than
|
41
|
+
# If you need to query a range, use greater than and less than constraints with Arel.
|
37
42
|
#
|
38
43
|
# Correct usage:
|
39
44
|
# query.where(query.table[:created_at].lteq(Date.today)).to_sql
|
40
45
|
# "SELECT * FROM \"table\" WHERE \"table\".\"created_at\" <= '2023-08-01'"
|
41
46
|
#
|
42
|
-
# This also supports array
|
47
|
+
# This also supports array constraints which will result in an IN query.
|
43
48
|
# query.where(entity_id: [1,2,3]).to_sql
|
44
49
|
# "SELECT * FROM \"table\" WHERE \"table\".\"entity_id\" IN (1, 2, 3)"
|
45
50
|
#
|
46
51
|
# Range support and more `Arel::Nodes` could be considered for future iterations.
|
47
52
|
# @return [ClickHouse::QueryBuilder] New instance of query builder.
|
48
|
-
def where(
|
49
|
-
|
53
|
+
def where(constraints)
|
54
|
+
validate_constraint_type!(constraints)
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
new_instance.conditions << conditions
|
54
|
-
else
|
55
|
-
add_conditions_to(new_instance, conditions)
|
56
|
-
end
|
56
|
+
clone.tap do |new_instance|
|
57
|
+
add_constraints_to(new_instance, constraints)
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
60
61
|
def select(*fields)
|
61
|
-
|
62
|
+
clone.tap do |new_instance|
|
62
63
|
existing_fields = new_instance.manager.projections.filter_map do |projection|
|
63
64
|
if projection.respond_to?(:to_s) && projection.to_s == '*'
|
64
65
|
nil
|
@@ -90,7 +91,7 @@ module ClickHouse
|
|
90
91
|
def order(field, direction = :asc)
|
91
92
|
validate_order_direction!(direction)
|
92
93
|
|
93
|
-
|
94
|
+
clone.tap do |new_instance|
|
94
95
|
order_node = case field
|
95
96
|
when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
|
96
97
|
field
|
@@ -104,7 +105,7 @@ module ClickHouse
|
|
104
105
|
end
|
105
106
|
|
106
107
|
def group(*columns)
|
107
|
-
|
108
|
+
clone.tap do |new_instance|
|
108
109
|
new_instance.manager.group(*columns)
|
109
110
|
end
|
110
111
|
end
|
@@ -120,7 +121,7 @@ module ClickHouse
|
|
120
121
|
end
|
121
122
|
|
122
123
|
def from(subquery, alias_name)
|
123
|
-
|
124
|
+
clone.tap do |new_instance|
|
124
125
|
if subquery.is_a?(self.class)
|
125
126
|
new_instance.manager.from(subquery.to_arel.as(alias_name))
|
126
127
|
else
|
@@ -129,9 +130,35 @@ module ClickHouse
|
|
129
130
|
end
|
130
131
|
end
|
131
132
|
|
132
|
-
def
|
133
|
-
|
133
|
+
def joins(table_name, constraint = nil)
|
134
|
+
clone.tap do |new_instance|
|
135
|
+
join_table = table_name.is_a?(Arel::Table) ? table_name : Arel::Table.new(table_name)
|
136
|
+
|
137
|
+
join_condition = case constraint
|
138
|
+
when Hash
|
139
|
+
# Handle hash based constraints like { table1.id: table2.ref_id } or {id: :ref_id}
|
140
|
+
constraint_conditions = constraint.map do |left, right|
|
141
|
+
left_field = left.is_a?(Arel::Attributes::Attribute) ? left : new_instance.table[left]
|
142
|
+
right_field = right.is_a?(Arel::Attributes::Attribute) ? right : join_table[right]
|
143
|
+
left_field.eq(right_field)
|
144
|
+
end
|
145
|
+
|
146
|
+
constraint_conditions.reduce(&:and)
|
147
|
+
when Proc
|
148
|
+
constraint.call(new_instance.table, join_table)
|
149
|
+
when Arel::Nodes::Node
|
150
|
+
constraint
|
151
|
+
end
|
152
|
+
|
153
|
+
if join_condition
|
154
|
+
new_instance.manager.join(join_table).on(join_condition)
|
155
|
+
else
|
156
|
+
new_instance.manager.join(join_table)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
134
160
|
|
161
|
+
def to_sql
|
135
162
|
visitor = Arel::Visitors::ToSql.new(ClickHouse::Client::ArelEngine.new)
|
136
163
|
visitor.accept(manager.ast, Arel::Collectors::SQLString.new).value
|
137
164
|
end
|
@@ -141,39 +168,43 @@ module ClickHouse
|
|
141
168
|
end
|
142
169
|
|
143
170
|
def to_arel
|
144
|
-
apply_conditions!
|
145
|
-
|
146
171
|
manager
|
147
172
|
end
|
148
173
|
|
149
174
|
private
|
150
175
|
|
151
|
-
def
|
152
|
-
return unless
|
176
|
+
def validate_constraint_type!(constraint)
|
177
|
+
return unless constraint.is_a?(Arel::Nodes::Node) && VALID_NODES.exclude?(constraint.class)
|
153
178
|
|
154
|
-
raise ArgumentError, "Unsupported Arel node type for QueryBuilder: #{
|
179
|
+
raise ArgumentError, "Unsupported Arel node type for QueryBuilder: #{constraint.class.name}"
|
155
180
|
end
|
156
181
|
|
157
|
-
def
|
158
|
-
|
159
|
-
instance.
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
182
|
+
def add_constraints_to(instance, constraints)
|
183
|
+
if constraints.is_a?(Arel::Nodes::Node)
|
184
|
+
instance.manager.where(constraints)
|
185
|
+
else
|
186
|
+
constraints.each do |key, value|
|
187
|
+
if value.is_a?(Hash)
|
188
|
+
# Handle nested hash for joined tables
|
189
|
+
join_table = Arel::Table.new(key)
|
190
|
+
value.each do |nested_key, nested_value|
|
191
|
+
build_arel_constraint(instance, join_table, nested_key, nested_value)
|
192
|
+
end
|
193
|
+
else
|
194
|
+
build_arel_constraint(instance, instance.table, key, value)
|
195
|
+
end
|
196
|
+
end
|
164
197
|
end
|
165
198
|
end
|
166
199
|
|
167
|
-
def
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
200
|
+
def build_arel_constraint(instance, table, key, value)
|
201
|
+
constraint = if value.is_a?(Array)
|
202
|
+
table[key].in(value)
|
203
|
+
else
|
204
|
+
table[key].eq(value)
|
205
|
+
end
|
173
206
|
|
174
|
-
|
175
|
-
manager.constraints.clear
|
176
|
-
conditions.each { |condition| manager.where(condition) }
|
207
|
+
instance.manager.where(constraint)
|
177
208
|
end
|
178
209
|
|
179
210
|
def validate_order_direction!(direction)
|
@@ -3,66 +3,71 @@
|
|
3
3
|
module ClickHouse
|
4
4
|
module Client
|
5
5
|
module Redactor
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
6
|
+
class << self
|
7
|
+
# Redacts the SQL query represented by the query builder.
|
8
|
+
#
|
9
|
+
# @param query_builder [::ClickHouse::Querybuilder] The query builder object to be redacted.
|
10
|
+
# @return [String] The redacted SQL query as a string.
|
11
|
+
# @raise [ArgumentError] when the condition in the query is of an unsupported type.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
# query_builder = ClickHouse::QueryBuilder.new('users').where(name: 'John Doe')
|
15
|
+
# redacted_query = ClickHouse::Redactor.redact(query_builder)
|
16
|
+
# # The redacted_query will contain the SQL query with values replaced by placeholders.
|
17
|
+
# output: "SELECT * FROM \"users\" WHERE \"users\".\"name\" = $1"
|
18
|
+
def redact(query_builder, bind_manager = ClickHouse::Client::BindIndexManager.new)
|
19
|
+
redacted_constraints = query_builder.manager.constraints.map do |constraint|
|
20
|
+
redact_constraint(constraint, bind_manager)
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
cloned_query_builder = query_builder.clone
|
24
|
+
|
25
|
+
cloned_query_builder.manager.constraints.clear
|
26
|
+
redacted_constraints.each do |constraint|
|
27
|
+
cloned_query_builder.manager.where(constraint)
|
28
|
+
end
|
23
29
|
|
24
|
-
|
25
|
-
cloned_query_builder.conditions.each do |condition|
|
26
|
-
cloned_query_builder.manager.where(condition)
|
30
|
+
cloned_query_builder.to_sql
|
27
31
|
end
|
28
32
|
|
29
|
-
|
30
|
-
visitor.accept(cloned_query_builder.manager.ast, Arel::Collectors::SQLString.new).value
|
31
|
-
end
|
33
|
+
private
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
35
|
+
def redact_constraint(constraint, bind_manager)
|
36
|
+
case constraint
|
37
|
+
when Arel::Nodes::In
|
38
|
+
constraint.left.in(Array.new(constraint.right.size) { Arel.sql(bind_manager.next_bind_str) })
|
39
|
+
when Arel::Nodes::Equality
|
40
|
+
constraint.left.eq(Arel.sql(bind_manager.next_bind_str))
|
41
|
+
when Arel::Nodes::LessThan
|
42
|
+
constraint.left.lt(Arel.sql(bind_manager.next_bind_str))
|
43
|
+
when Arel::Nodes::LessThanOrEqual
|
44
|
+
constraint.left.lteq(Arel.sql(bind_manager.next_bind_str))
|
45
|
+
when Arel::Nodes::GreaterThan
|
46
|
+
constraint.left.gt(Arel.sql(bind_manager.next_bind_str))
|
47
|
+
when Arel::Nodes::GreaterThanOrEqual
|
48
|
+
constraint.left.gteq(Arel.sql(bind_manager.next_bind_str))
|
49
|
+
when Arel::Nodes::NamedFunction
|
50
|
+
redact_named_function(constraint, bind_manager)
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Unsupported Arel node type for Redactor: #{constraint.class}"
|
53
|
+
end
|
51
54
|
end
|
52
|
-
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
|
56
|
+
def redact_named_function(constraint, bind_manager)
|
57
|
+
redacted_constraint =
|
58
|
+
Arel::Nodes::NamedFunction.new(constraint.name, constraint.expressions.dup)
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
case redacted_constraint.name
|
61
|
+
when 'startsWith'
|
62
|
+
redacted_constraint.expressions[1] = Arel.sql(bind_manager.next_bind_str)
|
63
|
+
else
|
64
|
+
redacted_constraint.expressions = redacted_constraint.expressions.map do
|
65
|
+
Arel.sql(bind_manager.next_bind_str)
|
66
|
+
end
|
67
|
+
end
|
64
68
|
|
65
|
-
|
69
|
+
redacted_constraint
|
70
|
+
end
|
66
71
|
end
|
67
72
|
end
|
68
73
|
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.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- group::optimize
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|