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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickHouse
4
+ module SQL
5
+ VERSION = "0.1.0"
6
+ end
7
+ end