clickhouse-sql 0.1.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 +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +25 -0
- data/README.md +426 -0
- data/lib/clickhouse/http_client/database.rb +42 -0
- data/lib/clickhouse/http_client/error.rb +18 -0
- data/lib/clickhouse/http_client/parameter_serializer.rb +127 -0
- data/lib/clickhouse/http_client/query.rb +54 -0
- data/lib/clickhouse/http_client/response_formatter.rb +78 -0
- data/lib/clickhouse/http_client/value_normalization.rb +29 -0
- data/lib/clickhouse/http_client.rb +288 -0
- data/lib/clickhouse/sql/bind_index_manager.rb +26 -0
- data/lib/clickhouse/sql/error.rb +13 -0
- data/lib/clickhouse/sql/fragment.rb +171 -0
- data/lib/clickhouse/sql/join.rb +70 -0
- data/lib/clickhouse/sql/part.rb +37 -0
- data/lib/clickhouse/sql/placeholder.rb +41 -0
- data/lib/clickhouse/sql/placeholder_type.rb +219 -0
- data/lib/clickhouse/sql/query.rb +139 -0
- data/lib/clickhouse/sql/raw.rb +53 -0
- data/lib/clickhouse/sql/select_item.rb +48 -0
- data/lib/clickhouse/sql/select_query.rb +393 -0
- data/lib/clickhouse/sql/setting.rb +89 -0
- data/lib/clickhouse/sql/table.rb +97 -0
- data/lib/clickhouse/sql/version.rb +7 -0
- data/lib/clickhouse/sql.rb +562 -0
- data/lib/clickhouse-sql.rb +5 -0
- metadata +133 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickHouse
|
|
4
|
+
module SQL
|
|
5
|
+
class SelectQuery
|
|
6
|
+
include Part
|
|
7
|
+
|
|
8
|
+
# Builds an empty mutable SELECT query.
|
|
9
|
+
#
|
|
10
|
+
# @return [ClickHouse::SQL::SelectQuery] Empty SELECT query builder.
|
|
11
|
+
def initialize
|
|
12
|
+
@selects = []
|
|
13
|
+
@distinct = false
|
|
14
|
+
@with = []
|
|
15
|
+
@joins = []
|
|
16
|
+
@wheres = []
|
|
17
|
+
@havings = []
|
|
18
|
+
@group_bys = []
|
|
19
|
+
@order_bys = []
|
|
20
|
+
@settings = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders `SELECT DISTINCT` instead of `SELECT`.
|
|
24
|
+
#
|
|
25
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
26
|
+
def distinct
|
|
27
|
+
@distinct = true
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Appends SELECT expressions.
|
|
32
|
+
#
|
|
33
|
+
# @param columns [Array<String, Symbol, ClickHouse::SQL::Part, nil>] SELECT expressions.
|
|
34
|
+
# @param aliases [Hash{Symbol,String=>String,Symbol,ClickHouse::SQL::Part}] Aliased expressions.
|
|
35
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
36
|
+
def select(*columns, **aliases)
|
|
37
|
+
columns.flatten.compact.each do |column|
|
|
38
|
+
@selects << SelectItem.new(expression: ClickHouse::SQL.coerce_raw_expression(column))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
aliases.each do |alias_name, expression|
|
|
42
|
+
@selects << SelectItem.new(expression: ClickHouse::SQL.coerce_raw_expression(expression), alias_name:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Appends a WITH/CTE fragment, or an array of WITH/CTE fragments.
|
|
49
|
+
#
|
|
50
|
+
# @param fragment [String, Symbol, ClickHouse::SQL::Part, Array, nil] WITH fragment(s).
|
|
51
|
+
# @param fills [Array<ClickHouse::SQL::Part>] Positional fills for anonymous `{}` slots.
|
|
52
|
+
# @param bindings [Hash] Placeholder bindings for the fragment.
|
|
53
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
54
|
+
def with(fragment = nil, *fills, **bindings)
|
|
55
|
+
append_clause(@with, fragment, fills, bindings)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Appends a CTE named by a table handle.
|
|
59
|
+
#
|
|
60
|
+
# @param handle [ClickHouse::SQL::Table] Handle for the CTE relation.
|
|
61
|
+
# @param query [String, Symbol, ClickHouse::SQL::Part] CTE query expression.
|
|
62
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
63
|
+
def with_cte(handle, query)
|
|
64
|
+
unless handle.is_a?(ClickHouse::SQL::Table)
|
|
65
|
+
raise Error, "CTE handle must be a table handle, got #{handle.class}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
name = ClickHouse::SQL.validate_alias!(handle.name)
|
|
69
|
+
@with << ClickHouse::SQL.fragment(
|
|
70
|
+
"{} AS ({})",
|
|
71
|
+
ClickHouse::SQL.raw_static(name),
|
|
72
|
+
ClickHouse::SQL.coerce_expression(query)
|
|
73
|
+
)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Sets the FROM table expression.
|
|
78
|
+
#
|
|
79
|
+
# A ClickHouse::SQL::Table handle owns its alias so column references
|
|
80
|
+
# built from the handle always match the rendered clause; passing `as:`
|
|
81
|
+
# alongside a handle raises.
|
|
82
|
+
#
|
|
83
|
+
# @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
|
|
84
|
+
# @param as [String, Symbol, nil] Optional table alias; only valid for
|
|
85
|
+
# string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
|
|
86
|
+
# @param final [Boolean] Whether to append ClickHouse `FINAL`.
|
|
87
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
88
|
+
def from(table, as: nil, final: false)
|
|
89
|
+
@from = ClickHouse::SQL.coerce_raw_expression(table)
|
|
90
|
+
@from_alias = ClickHouse::SQL.resolve_table_alias(table, as)
|
|
91
|
+
@from_final = final
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sets the FROM clause to a nested query.
|
|
96
|
+
#
|
|
97
|
+
# @param query [String, Symbol, ClickHouse::SQL::Part] Subquery expression.
|
|
98
|
+
# @param as [String, Symbol, ClickHouse::SQL::Table] Required table alias.
|
|
99
|
+
# A table handle contributes its qualifier so references built from
|
|
100
|
+
# the handle match the rendered alias.
|
|
101
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
102
|
+
def from_subquery(query, as:)
|
|
103
|
+
@from = ClickHouse::SQL.fragment("({query})", query: ClickHouse::SQL.coerce_expression(query))
|
|
104
|
+
@from_alias = as.is_a?(ClickHouse::SQL::Table) ? as.qualifier : as
|
|
105
|
+
@from_final = false
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Appends a JOIN clause.
|
|
110
|
+
#
|
|
111
|
+
# @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
|
|
112
|
+
# @param type [String] JOIN type, for example `INNER` or `LEFT`.
|
|
113
|
+
# @param as [String, Symbol, nil] Optional table alias; only valid for
|
|
114
|
+
# string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
|
|
115
|
+
# @param final [Boolean] Whether to append ClickHouse `FINAL`.
|
|
116
|
+
# @param on [String, ClickHouse::SQL::Part, Array] ON conditions.
|
|
117
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
118
|
+
def join(table, type: "INNER", as: nil, final: false, on:)
|
|
119
|
+
@joins << Join.new(table, type:, as:, final:, on:)
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Appends a LEFT JOIN clause.
|
|
124
|
+
#
|
|
125
|
+
# @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
|
|
126
|
+
# @param as [String, Symbol, nil] Optional table alias; only valid for
|
|
127
|
+
# string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
|
|
128
|
+
# @param final [Boolean] Whether to append ClickHouse `FINAL`.
|
|
129
|
+
# @param on [String, ClickHouse::SQL::Part, Array] ON conditions.
|
|
130
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
131
|
+
def left_join(table, as: nil, final: false, on:)
|
|
132
|
+
join(table, type: "LEFT", as:, final:, on:)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Appends an INNER JOIN clause.
|
|
136
|
+
#
|
|
137
|
+
# @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
|
|
138
|
+
# @param as [String, Symbol, nil] Optional table alias; only valid for
|
|
139
|
+
# string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
|
|
140
|
+
# @param final [Boolean] Whether to append ClickHouse `FINAL`.
|
|
141
|
+
# @param on [String, ClickHouse::SQL::Part, Array] ON conditions.
|
|
142
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
143
|
+
def inner_join(table, as: nil, final: false, on:)
|
|
144
|
+
join(table, type: "INNER", as:, final:, on:)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Appends pre-rendered JOIN fragments.
|
|
148
|
+
#
|
|
149
|
+
# @param joins [Array<String, Symbol, ClickHouse::SQL::Part, nil>] JOIN fragments.
|
|
150
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
151
|
+
def joins(*joins)
|
|
152
|
+
joins.flatten.compact.each do |join|
|
|
153
|
+
@joins << ClickHouse::SQL.coerce_expression(join)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Appends a WHERE condition, or an array of WHERE conditions, later
|
|
160
|
+
# rendered with `AND`.
|
|
161
|
+
#
|
|
162
|
+
# @param condition [String, Symbol, ClickHouse::SQL::Part, Array, nil] WHERE condition(s).
|
|
163
|
+
# @param fills [Array<ClickHouse::SQL::Part>] Positional fills for anonymous `{}` slots.
|
|
164
|
+
# @param bindings [Hash] Placeholder bindings for the condition.
|
|
165
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
166
|
+
def where(condition = nil, *fills, **bindings)
|
|
167
|
+
append_clause(@wheres, condition, fills, bindings)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Appends a HAVING condition, or an array of HAVING conditions, later
|
|
171
|
+
# rendered with `AND`.
|
|
172
|
+
#
|
|
173
|
+
# @param condition [String, Symbol, ClickHouse::SQL::Part, Array, nil] HAVING condition(s).
|
|
174
|
+
# @param fills [Array<ClickHouse::SQL::Part>] Positional fills for anonymous `{}` slots.
|
|
175
|
+
# @param bindings [Hash] Placeholder bindings for the condition.
|
|
176
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
177
|
+
def having(condition = nil, *fills, **bindings)
|
|
178
|
+
append_clause(@havings, condition, fills, bindings)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Appends GROUP BY expressions.
|
|
182
|
+
#
|
|
183
|
+
# @param expressions [Array<String, Symbol, ClickHouse::SQL::Part, nil>] GROUP BY expressions.
|
|
184
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
185
|
+
def group_by(*expressions)
|
|
186
|
+
append_raw_expressions(@group_bys, expressions)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Appends ORDER BY expressions.
|
|
190
|
+
#
|
|
191
|
+
# @param expressions [Array<String, Symbol, ClickHouse::SQL::Part, nil>] ORDER BY expressions.
|
|
192
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
193
|
+
def order_by(*expressions)
|
|
194
|
+
append_raw_expressions(@order_bys, expressions)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Sets the LIMIT expression.
|
|
198
|
+
#
|
|
199
|
+
# @param expression [String, Symbol, ClickHouse::SQL::Part, nil] LIMIT expression.
|
|
200
|
+
# @param bindings [Hash] Placeholder bindings for the LIMIT expression.
|
|
201
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
202
|
+
def limit(expression = nil, **bindings)
|
|
203
|
+
@limit = one_fragment(expression, bindings)
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Sets the OFFSET expression.
|
|
208
|
+
#
|
|
209
|
+
# @param expression [String, Symbol, ClickHouse::SQL::Part, nil] OFFSET expression.
|
|
210
|
+
# @param bindings [Hash] Placeholder bindings for the OFFSET expression.
|
|
211
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
212
|
+
def offset(expression = nil, **bindings)
|
|
213
|
+
@offset = one_fragment(expression, bindings)
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Appends static SETTINGS fragments.
|
|
218
|
+
#
|
|
219
|
+
# @param settings [Array<String, Symbol, ClickHouse::SQL::Part, nil>] SETTINGS fragments.
|
|
220
|
+
# @param bindings [Hash] Unsupported; ClickHouse does not parameterize SETTINGS.
|
|
221
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
222
|
+
def settings(*settings, **bindings)
|
|
223
|
+
if bindings.any?
|
|
224
|
+
raise Error, "ClickHouse SETTINGS do not support query parameters; use setting(name, value) for dynamic setting values"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
settings.flatten.compact.each { |setting| @settings << ClickHouse::SQL.coerce_expression(setting) }
|
|
228
|
+
validate_settings!
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Appends one validated SETTINGS assignment.
|
|
233
|
+
#
|
|
234
|
+
# @param name [String, Symbol] ClickHouse setting name.
|
|
235
|
+
# @param value [String, Integer, Float, Boolean] Inline setting value.
|
|
236
|
+
# @return [ClickHouse::SQL::SelectQuery] This query.
|
|
237
|
+
def setting(name, value)
|
|
238
|
+
@settings << Setting.new(name, value)
|
|
239
|
+
self
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Renders the SELECT query with ClickHouse typed placeholders intact.
|
|
243
|
+
#
|
|
244
|
+
# @return [String] SQL query text.
|
|
245
|
+
def to_sql
|
|
246
|
+
render(redacted: false)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Renders the SELECT query with typed placeholders replaced by bind markers.
|
|
250
|
+
#
|
|
251
|
+
# @param bind_index_manager [ClickHouse::SQL::BindIndexManager] Redacted bind index state.
|
|
252
|
+
# @return [String] Redacted SQL query text.
|
|
253
|
+
def to_redacted_sql(bind_index_manager = ClickHouse::SQL::BindIndexManager.new)
|
|
254
|
+
render(redacted: true, bind_index_manager:)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Returns typed placeholder values used by the SELECT query.
|
|
258
|
+
#
|
|
259
|
+
# @return [Hash{Symbol=>Object}] Placeholder values keyed by name.
|
|
260
|
+
def placeholders
|
|
261
|
+
ClickHouse::SQL.collect_placeholders(query_parts)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Returns ClickHouse placeholder types used by the SELECT query.
|
|
265
|
+
#
|
|
266
|
+
# @return [Hash{Symbol=>String}] Placeholder types keyed by name.
|
|
267
|
+
def placeholder_types
|
|
268
|
+
ClickHouse::SQL.collect_placeholder_types(query_parts)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Converts this builder into an immutable query object accepted by ClickHouse::HTTPClient.
|
|
272
|
+
#
|
|
273
|
+
# @return [ClickHouse::SQL::Query] Query object accepted by ClickHouse::HTTPClient.
|
|
274
|
+
def to_query
|
|
275
|
+
Query.new(
|
|
276
|
+
sql: to_sql,
|
|
277
|
+
redacted_sql: to_redacted_sql(Query.redacted_bind_index_manager),
|
|
278
|
+
placeholders:,
|
|
279
|
+
placeholder_types:
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
# Appends one clause entry: a single fragment (with optional positional
|
|
286
|
+
# fills for anonymous `{}` slots and keyword placeholder bindings), an
|
|
287
|
+
# array of fragments, or nil (appending nothing).
|
|
288
|
+
def append_clause(target, condition, fills, bindings)
|
|
289
|
+
case condition
|
|
290
|
+
when nil, Array
|
|
291
|
+
if fills.any? || bindings.any?
|
|
292
|
+
raise Error, "Positional fills and keyword placeholder values require a single SQL string, got #{condition.class}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Array(condition).flatten.compact.each { |fragment| target << ClickHouse::SQL.coerce_expression(fragment) }
|
|
296
|
+
when String
|
|
297
|
+
if fills.any? || bindings.any?
|
|
298
|
+
target << Fragment.new(condition, bindings, fills:)
|
|
299
|
+
else
|
|
300
|
+
target << ClickHouse::SQL.coerce_expression(condition)
|
|
301
|
+
end
|
|
302
|
+
else
|
|
303
|
+
if fills.any? || bindings.any?
|
|
304
|
+
raise Error, "Positional fills and keyword placeholder values require a single SQL string, got #{condition.class}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
target << ClickHouse::SQL.coerce_expression(condition)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
self
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def append_raw_expressions(target, expressions)
|
|
314
|
+
expressions.flatten.compact.each do |expression|
|
|
315
|
+
target << ClickHouse::SQL.coerce_raw_expression(expression)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
self
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def one_fragment(expression, bindings)
|
|
322
|
+
if bindings.any?
|
|
323
|
+
raise Error, "Keyword placeholder values require a SQL fragment" unless expression
|
|
324
|
+
|
|
325
|
+
Fragment.new(expression, bindings)
|
|
326
|
+
elsif expression
|
|
327
|
+
ClickHouse::SQL.coerce_expression(expression)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def validate_settings!
|
|
332
|
+
@settings.each do |setting|
|
|
333
|
+
next unless setting.respond_to?(:placeholder_types)
|
|
334
|
+
next if setting.placeholder_types.empty?
|
|
335
|
+
|
|
336
|
+
raise Error, "ClickHouse SETTINGS do not support query parameters; use setting(name, value) for dynamic setting values"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def render(redacted:, bind_index_manager: ClickHouse::SQL::BindIndexManager.new)
|
|
341
|
+
raise Error, "SELECT requires at least one selected expression" if @selects.empty?
|
|
342
|
+
raise Error, "SELECT requires a FROM clause" unless @from
|
|
343
|
+
|
|
344
|
+
parts = []
|
|
345
|
+
parts << render_with(redacted:, bind_index_manager:) if @with.any?
|
|
346
|
+
parts << render_select(redacted:, bind_index_manager:)
|
|
347
|
+
parts << render_from(redacted:, bind_index_manager:)
|
|
348
|
+
parts.concat(@joins.map { |join| ClickHouse::SQL.render_part(join, redacted:, bind_index_manager:) })
|
|
349
|
+
parts << render_conjunctive_clause("WHERE", @wheres, redacted:, bind_index_manager:) if @wheres.any?
|
|
350
|
+
parts << render_list_clause("GROUP BY", @group_bys, redacted:, bind_index_manager:) if @group_bys.any?
|
|
351
|
+
parts << render_conjunctive_clause("HAVING", @havings, redacted:, bind_index_manager:) if @havings.any?
|
|
352
|
+
parts << render_list_clause("ORDER BY", @order_bys, redacted:, bind_index_manager:) if @order_bys.any?
|
|
353
|
+
parts << "LIMIT #{ClickHouse::SQL.render_part(@limit, redacted:, bind_index_manager:)}" if @limit
|
|
354
|
+
parts << "OFFSET #{ClickHouse::SQL.render_part(@offset, redacted:, bind_index_manager:)}" if @offset
|
|
355
|
+
parts << render_list_clause("SETTINGS", @settings, redacted:, bind_index_manager:) if @settings.any?
|
|
356
|
+
|
|
357
|
+
parts.join("\n")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def render_with(redacted:, bind_index_manager:)
|
|
361
|
+
"WITH\n #{@with.map { |part| ClickHouse::SQL.render_part(part, redacted:, bind_index_manager:) }.join(",\n ")}"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def render_select(redacted:, bind_index_manager:)
|
|
365
|
+
keyword = @distinct ? "SELECT DISTINCT" : "SELECT"
|
|
366
|
+
"#{keyword}\n #{@selects.map { |select| ClickHouse::SQL.render_part(select, redacted:, bind_index_manager:) }.join(",\n ")}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def render_from(redacted:, bind_index_manager:)
|
|
370
|
+
sql = +"FROM #{ClickHouse::SQL.render_part(@from, redacted:, bind_index_manager:)}"
|
|
371
|
+
sql << " FINAL" if @from_final
|
|
372
|
+
sql << " AS #{ClickHouse::SQL.validate_alias!(@from_alias)}" if @from_alias
|
|
373
|
+
sql
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Each condition is parenthesized so a condition containing a top-level
|
|
377
|
+
# OR cannot change the meaning of the AND-joined clause.
|
|
378
|
+
def render_conjunctive_clause(keyword, fragments, redacted:, bind_index_manager:)
|
|
379
|
+
rendered = fragments.map { |fragment| "(#{ClickHouse::SQL.render_part(fragment, redacted:, bind_index_manager:)})" }
|
|
380
|
+
"#{keyword}\n #{rendered.join("\n AND ")}"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def render_list_clause(keyword, fragments, redacted:, bind_index_manager:)
|
|
384
|
+
rendered = fragments.map { |fragment| ClickHouse::SQL.render_part(fragment, redacted:, bind_index_manager:) }
|
|
385
|
+
"#{keyword} #{rendered.join(', ')}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def query_parts
|
|
389
|
+
@selects + @with + [@from] + @joins + @wheres + @group_bys + @havings + @order_bys + [@limit, @offset] + @settings
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickHouse
|
|
4
|
+
module SQL
|
|
5
|
+
class Setting
|
|
6
|
+
include Part
|
|
7
|
+
|
|
8
|
+
# Builds a validated ClickHouse SETTINGS assignment.
|
|
9
|
+
#
|
|
10
|
+
# @param name [String, Symbol] ClickHouse setting name.
|
|
11
|
+
# @param value [String, Integer, Float, Boolean] Inline setting value.
|
|
12
|
+
# @return [ClickHouse::SQL::Setting] A setting expression.
|
|
13
|
+
def initialize(name, value)
|
|
14
|
+
@name = ClickHouse::SQL.validate_setting_name!(name)
|
|
15
|
+
@value = validate_value(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Renders the setting assignment with its literal value.
|
|
19
|
+
#
|
|
20
|
+
# @return [String] SQL setting assignment.
|
|
21
|
+
def to_sql
|
|
22
|
+
"#{@name} = #{serialized_value}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Renders the setting assignment with its value redacted.
|
|
26
|
+
#
|
|
27
|
+
# @param bind_index_manager [ClickHouse::SQL::BindIndexManager] Redacted bind index state.
|
|
28
|
+
# @return [String] Redacted SQL setting assignment.
|
|
29
|
+
def to_redacted_sql(bind_index_manager = ClickHouse::SQL::BindIndexManager.new)
|
|
30
|
+
"#{@name} = #{bind_index_manager.next_bind_str}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns no placeholders because settings are rendered as literals.
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] Empty placeholder values.
|
|
36
|
+
def placeholders
|
|
37
|
+
{}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns no placeholder types because settings are rendered as literals.
|
|
41
|
+
#
|
|
42
|
+
# @return [Hash] Empty placeholder types.
|
|
43
|
+
def placeholder_types
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_value(value)
|
|
50
|
+
case value
|
|
51
|
+
when String, Integer, TrueClass, FalseClass
|
|
52
|
+
value
|
|
53
|
+
when Float
|
|
54
|
+
return value if value.finite?
|
|
55
|
+
|
|
56
|
+
raise Error, "ClickHouse setting value must be finite, got #{value.inspect}"
|
|
57
|
+
else
|
|
58
|
+
raise Error, "ClickHouse setting value must be a String, Integer, Float, or boolean, got #{value.class}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def serialized_value
|
|
63
|
+
case @value
|
|
64
|
+
when String
|
|
65
|
+
quote_and_escape_string(@value)
|
|
66
|
+
when TrueClass
|
|
67
|
+
"1"
|
|
68
|
+
when FalseClass
|
|
69
|
+
"0"
|
|
70
|
+
else
|
|
71
|
+
@value.to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Renders a ClickHouse single-quoted string literal, escaping each
|
|
76
|
+
# single-quote and backslash by prefixing it with a backslash.
|
|
77
|
+
# https://clickhouse.com/docs/sql-reference/syntax#string
|
|
78
|
+
def quote_and_escape_string(str)
|
|
79
|
+
# Note the double-escaping in the replacement string:
|
|
80
|
+
# - Ruby strings consume one backslash
|
|
81
|
+
# - ruby syntax "\\" => ruby string "\"
|
|
82
|
+
# - Regexp replacement consumes one backslash
|
|
83
|
+
# - ruby literal syntax \\\\ => ruby string \\ => regexp replacement \
|
|
84
|
+
# - ruby literal syntax \\0 => ruby string \0 => regexp replacement match #0
|
|
85
|
+
"'#{str.gsub(/['\\]/, "\\\\\\0")}'"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickHouse
|
|
4
|
+
module SQL
|
|
5
|
+
# A validated table reference that qualifies column references.
|
|
6
|
+
#
|
|
7
|
+
# A table handle renders as its table name in FROM/JOIN position, and
|
|
8
|
+
# builds qualified column references via `#[]` so shared fragments stay
|
|
9
|
+
# unambiguous when a query joins tables with overlapping column names.
|
|
10
|
+
#
|
|
11
|
+
# @example Qualified columns from a table handle
|
|
12
|
+
# events = CH.table("events")
|
|
13
|
+
# accounts = CH.table("accounts", as: "a")
|
|
14
|
+
#
|
|
15
|
+
# CH.select(events[:account_id])
|
|
16
|
+
# .from(events)
|
|
17
|
+
# .left_join(accounts, final: true, on: [
|
|
18
|
+
# CH.fragment("{} = {}", accounts[:id], events[:account_id]),
|
|
19
|
+
# ])
|
|
20
|
+
# .where("{} >= {from:DateTime64(6)}", events[:timestamp], from: from_time)
|
|
21
|
+
class Table
|
|
22
|
+
include Part
|
|
23
|
+
|
|
24
|
+
# @return [String] Validated table name.
|
|
25
|
+
attr_reader :name
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] Validated table alias, if any.
|
|
28
|
+
attr_reader :alias_name
|
|
29
|
+
|
|
30
|
+
# Builds a validated table handle.
|
|
31
|
+
#
|
|
32
|
+
# @param name [String, Symbol, #to_s] Trusted table name.
|
|
33
|
+
# @param as [String, Symbol, nil] Optional table alias.
|
|
34
|
+
# @return [ClickHouse::SQL::Table] A validated table handle.
|
|
35
|
+
def initialize(name, as: nil)
|
|
36
|
+
@name = validate_identifier!(name)
|
|
37
|
+
@alias_name = as.nil? ? nil : ClickHouse::SQL.validate_alias!(as)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Builds a qualified column reference using the alias when present.
|
|
41
|
+
#
|
|
42
|
+
# @param column [String, Symbol, #to_s] Trusted column name or nested path.
|
|
43
|
+
# @return [ClickHouse::SQL::Raw] Qualified column expression.
|
|
44
|
+
def [](column)
|
|
45
|
+
Raw.new("#{qualifier}.#{validate_identifier!(column)}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Builds qualified column references for many columns at once.
|
|
49
|
+
#
|
|
50
|
+
# Accepts names as arguments or arrays (`t.columns(%i[id timestamp])`).
|
|
51
|
+
# SelectQuery#select flattens arrays, so the result can be passed with or
|
|
52
|
+
# without a splat.
|
|
53
|
+
#
|
|
54
|
+
# @param names [Array<String, Symbol, Array>] Trusted column names or nested paths.
|
|
55
|
+
# @return [Array<ClickHouse::SQL::Raw>] Qualified column expressions.
|
|
56
|
+
def columns(*names)
|
|
57
|
+
names.flatten.map { |name| self[name] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the name this table's columns are qualified with.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] The table alias when present, otherwise the table name.
|
|
63
|
+
def qualifier
|
|
64
|
+
@alias_name || @name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Renders the table name for FROM/JOIN position.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] The table name.
|
|
70
|
+
def to_sql
|
|
71
|
+
@name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Renders the table name for redacted logs.
|
|
75
|
+
#
|
|
76
|
+
# @param _bind_index_manager [ClickHouse::SQL::BindIndexManager] Unused redacted bind index state.
|
|
77
|
+
# @return [String] The table name.
|
|
78
|
+
def to_redacted_sql(_bind_index_manager = ClickHouse::SQL::BindIndexManager.new)
|
|
79
|
+
@name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Table handles interpolate identifiers into raw SQL Ruby-side with no
|
|
85
|
+
# quoting, unlike `{x:Identifier}` placeholders which ClickHouse
|
|
86
|
+
# substitutes server-side with proper quoting. This regex is the only
|
|
87
|
+
# defense, so it is stricter than the Identifier placeholder validation:
|
|
88
|
+
# each dot-separated segment must be a simple identifier.
|
|
89
|
+
def validate_identifier!(value)
|
|
90
|
+
value = value.to_s
|
|
91
|
+
return value if value.match?(QUALIFIED_IDENTIFIER_REGEX)
|
|
92
|
+
|
|
93
|
+
raise Error, "Table identifier must be a safe identifier: #{ClickHouse::SQL.truncated_inspect(value)}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|