chc-r8 0.3.2
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 +7 -0
- data/.github/workflows/publish.yml +47 -0
- data/.gitlab-ci.yml +73 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +40 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +179 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/click_house-client.gemspec +33 -0
- data/lib/click_house/client/arel_engine.rb +19 -0
- data/lib/click_house/client/bind_index_manager.rb +17 -0
- data/lib/click_house/client/configuration.rb +78 -0
- data/lib/click_house/client/database.rb +48 -0
- data/lib/click_house/client/formatter.rb +35 -0
- data/lib/click_house/client/query.rb +82 -0
- data/lib/click_house/client/query_builder.rb +162 -0
- data/lib/click_house/client/query_like.rb +31 -0
- data/lib/click_house/client/quoting.rb +25 -0
- data/lib/click_house/client/redactor.rb +69 -0
- data/lib/click_house/client/response.rb +19 -0
- data/lib/click_house/client/version.rb +7 -0
- data/lib/click_house/client.rb +137 -0
- metadata +192 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class Configuration
|
6
|
+
# Configuration options:
|
7
|
+
#
|
8
|
+
# *register_database* (method): registers a database, the following arguments are required:
|
9
|
+
# - database: database name
|
10
|
+
# - url: URL and port to the HTTP interface
|
11
|
+
# - username
|
12
|
+
# - password
|
13
|
+
# - variables (optional): configuration for the client
|
14
|
+
#
|
15
|
+
# *http_post_proc*: A callable object for invoking the HTTP request.
|
16
|
+
# The object must handle the following parameters: url, headers, body
|
17
|
+
# and return a ClickHouse::Client::Response object.
|
18
|
+
#
|
19
|
+
# *json_parser*: object for parsing JSON strings, it should respond to the "parse" method
|
20
|
+
#
|
21
|
+
# *logger*: object for receiving logger commands. Default `$stdout`
|
22
|
+
# *log_proc*: any output (e.g. structure) to wrap around the query for every statement
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# ClickHouse::Client.configure do |c|
|
27
|
+
# c.register_database(:main,
|
28
|
+
# database: 'gitlab_clickhouse_test',
|
29
|
+
# url: 'http://localhost:8123',
|
30
|
+
# username: 'default',
|
31
|
+
# password: 'clickhouse',
|
32
|
+
# variables: {
|
33
|
+
# join_use_nulls: 1 # treat JOINs as per SQL standard
|
34
|
+
# }
|
35
|
+
# )
|
36
|
+
#
|
37
|
+
# c.logger = MyLogger.new
|
38
|
+
# c.log_proc = ->(query) do
|
39
|
+
# { query_body: query.to_redacted_sql }
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# c.http_post_proc = lambda do |url, headers, body|
|
43
|
+
# options = {
|
44
|
+
# headers: headers,
|
45
|
+
# body: body,
|
46
|
+
# allow_local_requests: false
|
47
|
+
# }
|
48
|
+
#
|
49
|
+
# response = Gitlab::HTTP.post(url, options)
|
50
|
+
# ClickHouse::Client::Response.new(response.body, response.code)
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# c.json_parser = JSON
|
54
|
+
# end
|
55
|
+
attr_accessor :http_post_proc, :json_parser, :logger, :log_proc
|
56
|
+
attr_reader :databases
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@databases = {}
|
60
|
+
@http_post_proc = nil
|
61
|
+
@json_parser = JSON
|
62
|
+
@logger = ::Logger.new($stdout)
|
63
|
+
@log_proc = ->(query) { query.to_sql }
|
64
|
+
end
|
65
|
+
|
66
|
+
def register_database(name, **args)
|
67
|
+
raise ConfigurationError, "The database '#{name}' is already registered" if @databases.key?(name)
|
68
|
+
|
69
|
+
@databases[name] = Database.new(**args)
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate!
|
73
|
+
raise ConfigurationError, "The 'http_post_proc' option is not configured" unless @http_post_proc
|
74
|
+
raise ConfigurationError, "The 'json_parser' option is not configured" unless @json_parser
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class Database
|
6
|
+
attr_reader :database
|
7
|
+
|
8
|
+
def initialize(database:, url:, username:, password:, variables: {})
|
9
|
+
@database = database
|
10
|
+
@url = url
|
11
|
+
@username = username
|
12
|
+
@password = password
|
13
|
+
@variables = {
|
14
|
+
database:,
|
15
|
+
enable_http_compression: 1 # enable HTTP compression by default
|
16
|
+
}.merge(variables).freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def uri
|
20
|
+
@uri ||= build_custom_uri
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_custom_uri(extra_variables: {})
|
24
|
+
parsed = Addressable::URI.parse(@url)
|
25
|
+
parsed.query_values = @variables.merge(extra_variables)
|
26
|
+
parsed
|
27
|
+
end
|
28
|
+
|
29
|
+
def headers
|
30
|
+
@headers ||= {
|
31
|
+
'X-ClickHouse-User' => @username,
|
32
|
+
'X-ClickHouse-Key' => @password,
|
33
|
+
'X-ClickHouse-Format' => 'JSON' # always return JSON data
|
34
|
+
}.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_default_database
|
38
|
+
self.class.new(
|
39
|
+
database: 'default',
|
40
|
+
url: @url,
|
41
|
+
username: @username,
|
42
|
+
password: @password,
|
43
|
+
variables: @variables.merge(database: 'default')
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class Formatter
|
6
|
+
DEFAULT = ->(value) { value }
|
7
|
+
|
8
|
+
BASIC_TYPE_CASTERS = {
|
9
|
+
'UInt64' => ->(value) { Integer(value) },
|
10
|
+
"DateTime64(6, 'UTC')" => ->(value) { ActiveSupport::TimeZone['UTC'].parse(value) },
|
11
|
+
"IntervalSecond" => ->(value) { ActiveSupport::Duration.build(value.to_i) },
|
12
|
+
"IntervalMillisecond" => ->(value) { ActiveSupport::Duration.build(value.to_i / 1000.0) }
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
TYPE_CASTERS = BASIC_TYPE_CASTERS.merge(
|
16
|
+
BASIC_TYPE_CASTERS.transform_keys { |type| "Nullable(#{type})" }
|
17
|
+
.transform_values { |caster| ->(value) { value.nil? ? nil : caster.call(value) } }
|
18
|
+
)
|
19
|
+
|
20
|
+
def self.format(result)
|
21
|
+
name_type_mapping = result['meta'].each_with_object({}) do |column, hash|
|
22
|
+
hash[column['name']] = column['type']
|
23
|
+
end
|
24
|
+
|
25
|
+
result['data'].map do |row|
|
26
|
+
row.each_with_object({}) do |(column, value), casted_row|
|
27
|
+
caster = TYPE_CASTERS.fetch(name_type_mapping[column], DEFAULT)
|
28
|
+
|
29
|
+
casted_row[column] = caster.call(value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class Query < QueryLike
|
6
|
+
SUBQUERY_PLACEHOLDER_REGEX = /{\w+:Subquery}/ # example: {var:Subquery}, special "internal" type for subqueries
|
7
|
+
PLACEHOLDER_REGEX = /{\w+:(\w|[()])+}/ # example: {var:UInt8} or {var:Array(UInt8)}
|
8
|
+
PLACEHOLDER_NAME_REGEX = /{(\w+):/ # example: {var:UInt8} => var
|
9
|
+
|
10
|
+
def self.build(query)
|
11
|
+
return query if query.is_a?(ClickHouse::Client::QueryLike)
|
12
|
+
|
13
|
+
new(raw_query: query)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(raw_query:, placeholders: {})
|
17
|
+
raise QueryError, 'Empty query string given' if raw_query.blank?
|
18
|
+
|
19
|
+
@raw_query = raw_query
|
20
|
+
@placeholders = placeholders || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# List of placeholders to be sent to ClickHouse for replacement.
|
24
|
+
# If there are subqueries, merge their placeholders as well.
|
25
|
+
def prepared_placeholders
|
26
|
+
all_placeholders = placeholders.select { |_, v| !v.is_a?(QueryLike) }
|
27
|
+
all_placeholders.transform_values! { |v| prepared_placeholder_value(v) }
|
28
|
+
|
29
|
+
placeholders.each_value do |value|
|
30
|
+
next unless value.is_a?(QueryLike)
|
31
|
+
|
32
|
+
all_placeholders.merge!(value.prepared_placeholders) do |key, a, b|
|
33
|
+
raise QueryError, "mismatching values for the '#{key}' placeholder: #{a} vs #{b}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
all_placeholders
|
38
|
+
end
|
39
|
+
|
40
|
+
# Placeholder replacement is handled by ClickHouse, only subquery placeholders
|
41
|
+
# will be replaced.
|
42
|
+
def to_sql
|
43
|
+
raw_query.gsub(SUBQUERY_PLACEHOLDER_REGEX) do |placeholder_in_query|
|
44
|
+
value = placeholder_value(placeholder_in_query)
|
45
|
+
|
46
|
+
if value.is_a?(QueryLike)
|
47
|
+
value.to_sql
|
48
|
+
else
|
49
|
+
placeholder_in_query
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_redacted_sql(bind_index_manager = BindIndexManager.new)
|
55
|
+
raw_query.gsub(PLACEHOLDER_REGEX) do |placeholder_in_query|
|
56
|
+
value = placeholder_value(placeholder_in_query)
|
57
|
+
|
58
|
+
if value.is_a?(QueryLike)
|
59
|
+
value.to_redacted_sql(bind_index_manager)
|
60
|
+
else
|
61
|
+
bind_index_manager.next_bind_str
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :raw_query, :placeholders
|
69
|
+
|
70
|
+
def placeholder_value(placeholder_in_query)
|
71
|
+
placeholder = placeholder_in_query[PLACEHOLDER_NAME_REGEX, 1]
|
72
|
+
placeholders.fetch(placeholder.to_sym)
|
73
|
+
end
|
74
|
+
|
75
|
+
def prepared_placeholder_value(value)
|
76
|
+
return value unless value.is_a?(Array)
|
77
|
+
|
78
|
+
Quoting.quote(value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module ClickHouse
|
6
|
+
module Client
|
7
|
+
class QueryBuilder < QueryLike
|
8
|
+
attr_reader :table
|
9
|
+
attr_accessor :conditions, :manager
|
10
|
+
|
11
|
+
VALID_NODES = [
|
12
|
+
Arel::Nodes::In,
|
13
|
+
Arel::Nodes::Equality,
|
14
|
+
Arel::Nodes::LessThan,
|
15
|
+
Arel::Nodes::LessThanOrEqual,
|
16
|
+
Arel::Nodes::GreaterThan,
|
17
|
+
Arel::Nodes::GreaterThanOrEqual,
|
18
|
+
Arel::Nodes::NamedFunction,
|
19
|
+
Arel::Nodes::NotIn,
|
20
|
+
Arel::Nodes::NotEqual,
|
21
|
+
Arel::Nodes::Between,
|
22
|
+
Arel::Nodes::And,
|
23
|
+
Arel::Nodes::Or,
|
24
|
+
Arel::Nodes::Grouping
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def initialize(table_name)
|
28
|
+
@table = Arel::Table.new(table_name)
|
29
|
+
@manager = Arel::SelectManager.new(Arel::Table.engine).from(@table).project(Arel.star)
|
30
|
+
@conditions = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# The `where` method currently only supports IN and equal to queries along
|
34
|
+
# with above listed VALID_NODES.
|
35
|
+
# 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 conditions with Arel.
|
37
|
+
#
|
38
|
+
# Correct usage:
|
39
|
+
# query.where(query.table[:created_at].lteq(Date.today)).to_sql
|
40
|
+
# "SELECT * FROM \"table\" WHERE \"table\".\"created_at\" <= '2023-08-01'"
|
41
|
+
#
|
42
|
+
# This also supports array conditions which will result in an IN query.
|
43
|
+
# query.where(entity_id: [1,2,3]).to_sql
|
44
|
+
# "SELECT * FROM \"table\" WHERE \"table\".\"entity_id\" IN (1, 2, 3)"
|
45
|
+
#
|
46
|
+
# Range support and more `Arel::Nodes` could be considered for future iterations.
|
47
|
+
# @return [ClickHouse::QueryBuilder] New instance of query builder.
|
48
|
+
def where(conditions)
|
49
|
+
validate_condition_type!(conditions)
|
50
|
+
|
51
|
+
deep_clone.tap do |new_instance|
|
52
|
+
if conditions.is_a?(Arel::Nodes::Node)
|
53
|
+
new_instance.conditions << conditions
|
54
|
+
else
|
55
|
+
add_conditions_to(new_instance, conditions)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def select(*fields)
|
61
|
+
deep_clone.tap do |new_instance|
|
62
|
+
existing_fields = new_instance.manager.projections.filter_map do |projection|
|
63
|
+
if projection.is_a?(Arel::Attributes::Attribute)
|
64
|
+
projection.name.to_s
|
65
|
+
elsif projection.to_s == '*'
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
new_projections = (existing_fields + fields).map do |field|
|
71
|
+
if field.is_a?(Symbol)
|
72
|
+
field.to_s
|
73
|
+
else
|
74
|
+
field
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
new_instance.manager.projections = new_projections.uniq.map do |field|
|
79
|
+
if field.is_a?(Arel::Expressions)
|
80
|
+
field
|
81
|
+
else
|
82
|
+
new_instance.table[field.to_s]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def order(field, direction = :asc)
|
89
|
+
validate_order_direction!(direction)
|
90
|
+
|
91
|
+
deep_clone.tap do |new_instance|
|
92
|
+
table_field = new_instance.table[field]
|
93
|
+
new_order = direction.to_s.casecmp('desc').zero? ? table_field.desc : table_field.asc
|
94
|
+
new_instance.manager.order(new_order)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def group(*columns)
|
99
|
+
deep_clone.tap do |new_instance|
|
100
|
+
new_instance.manager.group(*columns)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def limit(count)
|
105
|
+
manager.take(count)
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def offset(count)
|
110
|
+
manager.skip(count)
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_sql
|
115
|
+
apply_conditions!
|
116
|
+
|
117
|
+
visitor = Arel::Visitors::ToSql.new(ClickHouse::Client::ArelEngine.new)
|
118
|
+
visitor.accept(manager.ast, Arel::Collectors::SQLString.new).value
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_redacted_sql(bind_index_manager = ClickHouse::Client::BindIndexManager.new)
|
122
|
+
ClickHouse::Client::Redactor.redact(self, bind_index_manager)
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def validate_condition_type!(condition)
|
128
|
+
return unless condition.is_a?(Arel::Nodes::Node) && VALID_NODES.exclude?(condition.class)
|
129
|
+
|
130
|
+
raise ArgumentError, "Unsupported Arel node type for QueryBuilder: #{condition.class.name}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_conditions_to(instance, conditions)
|
134
|
+
conditions.each do |key, value|
|
135
|
+
instance.conditions << if value.is_a?(Array)
|
136
|
+
instance.table[key].in(value)
|
137
|
+
else
|
138
|
+
instance.table[key].eq(value)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def deep_clone
|
144
|
+
self.class.new(table.name).tap do |new_instance|
|
145
|
+
new_instance.manager = manager.clone
|
146
|
+
new_instance.conditions = conditions.map(&:clone)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def apply_conditions!
|
151
|
+
manager.constraints.clear
|
152
|
+
conditions.each { |condition| manager.where(condition) }
|
153
|
+
end
|
154
|
+
|
155
|
+
def validate_order_direction!(direction)
|
156
|
+
return if %w[asc desc].include?(direction.to_s.downcase)
|
157
|
+
|
158
|
+
raise ArgumentError, "Invalid order direction '#{direction}'. Must be :asc or :desc"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class QueryLike
|
6
|
+
# Build a SQL string that can be executed on a ClickHouse database.
|
7
|
+
def to_sql
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Redacted version of the SQL query generated by the to_sql method where the
|
12
|
+
# placeholders are stripped. These queries are meant to be exported to external
|
13
|
+
# log aggregation systems.
|
14
|
+
def to_redacted_sql(bind_index_manager = BindIndexManager.new)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override when placeholders should be supported
|
19
|
+
def prepared_placeholders
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Override when placeholders should be supported
|
26
|
+
def placeholders
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
module Quoting
|
6
|
+
class << self
|
7
|
+
def quote(value)
|
8
|
+
case value
|
9
|
+
when Numeric then value.to_s
|
10
|
+
when String, Symbol then "'#{value.to_s.gsub('\\', '\&\&').gsub("'", "''")}'"
|
11
|
+
when Array then "[#{value.map { |v| quote(v) }.join(',')}]"
|
12
|
+
when nil then "NULL"
|
13
|
+
else quote_str(value.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def quote_str(value)
|
20
|
+
"'#{value.gsub("'", "''")}'"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
module Redactor
|
6
|
+
# Redacts the SQL query represented by the query builder.
|
7
|
+
#
|
8
|
+
# @param query_builder [::ClickHouse::Querybuilder] The query builder object to be redacted.
|
9
|
+
# @return [String] The redacted SQL query as a string.
|
10
|
+
# @raise [ArgumentError] when the condition in the query is of an unsupported type.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# query_builder = ClickHouse::QueryBuilder.new('users').where(name: 'John Doe')
|
14
|
+
# redacted_query = ClickHouse::Redactor.redact(query_builder)
|
15
|
+
# # The redacted_query will contain the SQL query with values replaced by placeholders.
|
16
|
+
# output: "SELECT * FROM \"users\" WHERE \"users\".\"name\" = $1"
|
17
|
+
def self.redact(query_builder, bind_manager = ClickHouse::Client::BindIndexManager.new)
|
18
|
+
cloned_query_builder = query_builder.clone
|
19
|
+
|
20
|
+
cloned_query_builder.conditions = cloned_query_builder.conditions.map do |condition|
|
21
|
+
redact_condition(condition, bind_manager)
|
22
|
+
end
|
23
|
+
|
24
|
+
cloned_query_builder.manager.constraints.clear
|
25
|
+
cloned_query_builder.conditions.each do |condition|
|
26
|
+
cloned_query_builder.manager.where(condition)
|
27
|
+
end
|
28
|
+
|
29
|
+
visitor = Arel::Visitors::ToSql.new(ClickHouse::Client::ArelEngine.new)
|
30
|
+
visitor.accept(cloned_query_builder.manager.ast, Arel::Collectors::SQLString.new).value
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.redact_condition(condition, bind_manager)
|
34
|
+
case condition
|
35
|
+
when Arel::Nodes::In
|
36
|
+
condition.left.in(Array.new(condition.right.size) { Arel.sql(bind_manager.next_bind_str) })
|
37
|
+
when Arel::Nodes::Equality
|
38
|
+
condition.left.eq(Arel.sql(bind_manager.next_bind_str))
|
39
|
+
when Arel::Nodes::LessThan
|
40
|
+
condition.left.lt(Arel.sql(bind_manager.next_bind_str))
|
41
|
+
when Arel::Nodes::LessThanOrEqual
|
42
|
+
condition.left.lteq(Arel.sql(bind_manager.next_bind_str))
|
43
|
+
when Arel::Nodes::GreaterThan
|
44
|
+
condition.left.gt(Arel.sql(bind_manager.next_bind_str))
|
45
|
+
when Arel::Nodes::GreaterThanOrEqual
|
46
|
+
condition.left.gteq(Arel.sql(bind_manager.next_bind_str))
|
47
|
+
when Arel::Nodes::NamedFunction
|
48
|
+
redact_named_function(condition, bind_manager)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Unsupported Arel node type for Redactor: #{condition.class}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.redact_named_function(condition, bind_manager)
|
55
|
+
redacted_condition =
|
56
|
+
Arel::Nodes::NamedFunction.new(condition.name, condition.expressions.dup)
|
57
|
+
|
58
|
+
case redacted_condition.name
|
59
|
+
when 'startsWith'
|
60
|
+
redacted_condition.expressions[1] = Arel.sql(bind_manager.next_bind_str)
|
61
|
+
else
|
62
|
+
redacted_condition.expressions = redacted_condition.expressions.map { Arel.sql(bind_manager.next_bind_str) }
|
63
|
+
end
|
64
|
+
|
65
|
+
redacted_condition
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickHouse
|
4
|
+
module Client
|
5
|
+
class Response
|
6
|
+
attr_reader :body, :headers
|
7
|
+
|
8
|
+
def initialize(body, http_status_code, headers = {})
|
9
|
+
@body = body
|
10
|
+
@http_status_code = http_status_code
|
11
|
+
@headers = headers
|
12
|
+
end
|
13
|
+
|
14
|
+
def success?
|
15
|
+
@http_status_code == 200
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|