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,562 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ require_relative "sql/error"
6
+ require_relative "sql/bind_index_manager"
7
+ require_relative "sql/part"
8
+ require_relative "sql/query"
9
+ require_relative "sql/placeholder"
10
+ require_relative "sql/select_item"
11
+ require_relative "sql/raw"
12
+ require_relative "sql/setting"
13
+ require_relative "sql/placeholder_type"
14
+ require_relative "sql/fragment"
15
+ require_relative "sql/table"
16
+ require_relative "sql/join"
17
+ require_relative "sql/select_query"
18
+
19
+ module ClickHouse
20
+ # Builds composable ClickHouse SELECT queries for ClickHouse::HTTPClient.
21
+ #
22
+ # Keep ClickHouse SQL visible, but pass runtime values through named typed
23
+ # placeholders (`{name:Type}`). Untyped placeholders are fragment slots for
24
+ # trusted SQL structure such as column references or subqueries: anonymous
25
+ # `{}` slots are filled by positional arguments in source order, and named
26
+ # `{name}` slots by keyword arguments.
27
+ #
28
+ # Table handles (ClickHouse::SQL.table) build validated, qualified column
29
+ # references so shared fragments stay unambiguous when queries join tables
30
+ # with overlapping column names.
31
+ #
32
+ # @example Build and execute a typed SELECT query joining two tables
33
+ # CH = ClickHouse::SQL
34
+ #
35
+ # events = CH.table("events")
36
+ # accounts = CH.table("accounts", as: "a")
37
+ #
38
+ # query = CH
39
+ # .select(events.columns(:id, :timestamp), accounts[:status])
40
+ # .from(events)
41
+ # .left_join(accounts, final: true, on: [
42
+ # CH.fragment("{} = {}", accounts[:id], events[:account_id]),
43
+ # ])
44
+ # .where("{} = {account_id:UUID}", events[:account_id], account_id: account_id)
45
+ # .where("{}.{tag_key:Identifier} = {value:String}", events[:tags], tag_key: "geo.country", value: "AU")
46
+ #
47
+ # ClickHouse::HTTPClient.select(query.to_query, :analytics)
48
+ module SQL
49
+ UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
50
+
51
+ # Validates `{x:Identifier}` placeholder values, which ClickHouse
52
+ # substitutes server-side with proper identifier quoting; permissive
53
+ # enough for hyphenated tag keys.
54
+ IDENTIFIER_REGEX = /\A[A-Za-z_][0-9A-Za-z_.-]*\z/
55
+
56
+ # Validates identifiers that Ruby interpolates into raw SQL with no
57
+ # quoting (table handles). Stricter than IDENTIFIER_REGEX: each
58
+ # dot-separated segment must be a simple identifier, so `-` (a SQL
59
+ # comment starter or subtraction) and stray dots never reach SQL text.
60
+ QUALIFIED_IDENTIFIER_REGEX = /\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\z/
61
+ GENERATED_BIND_PREFIX_REGEX = /\A[A-Za-z_]\w*\z/
62
+ # Braces would terminate a generated placeholder early (or misalign the
63
+ # parser), smuggling the remainder of the type into the SQL as raw text,
64
+ # so a generated bind type must not contain them. Anything else stays
65
+ # inside the placeholder and is validated downstream.
66
+ GENERATED_BIND_TYPE_REGEX = /\A[^{}]+\z/
67
+ SQL_ALIAS_REGEX = /\A[A-Za-z_]\w*\z/
68
+ SETTING_NAME_REGEX = /\A[A-Za-z_]\w*\z/
69
+
70
+ module ValueNormalization
71
+ BLANK_STRING = /\A[[:space:]]*\z/
72
+
73
+ def self.blank_string?(value)
74
+ !value || BLANK_STRING.match?(value.to_s)
75
+ end
76
+
77
+ def self.symbolize_keys(hash)
78
+ hash.transform_keys do |key|
79
+ key.respond_to?(:to_sym) ? key.to_sym : key
80
+ end
81
+ end
82
+ end
83
+ private_constant :ValueNormalization
84
+
85
+ module_function
86
+
87
+ # Builds a SQL expression with typed placeholder values and fragment slots.
88
+ #
89
+ # Anonymous `{}` slots are filled in source order by positional SQL parts.
90
+ # Named `{name}` slots and `{name:Type}` values take keyword bindings.
91
+ #
92
+ # @param sql [String, #to_s] SQL containing `{name:Type}` value placeholders,
93
+ # `{name}` fragment slots, and/or anonymous `{}` fragment slots.
94
+ # @param args [Array<ClickHouse::SQL::Part, Hash>] Positional fills for `{}`
95
+ # slots in source order; a Hash argument is treated as named bindings.
96
+ # @param keyword_bindings [Hash] Additional placeholder values keyed by name.
97
+ # @return [ClickHouse::SQL::Fragment] A composable SQL fragment.
98
+ def fragment(sql, *args, **keyword_bindings)
99
+ hashes, fills = args.partition { |arg| arg.is_a?(Hash) }
100
+ bindings = hashes.reduce({}, :merge)
101
+ Fragment.new(sql, merge_bindings(bindings, keyword_bindings), fills:)
102
+ end
103
+
104
+ # Wraps trusted static SQL that has no placeholder values.
105
+ #
106
+ # @param sql [String, #to_s] Trusted SQL structure or expression.
107
+ # @return [ClickHouse::SQL::Raw] A raw SQL expression.
108
+ def raw_static(sql)
109
+ Raw.new(sql)
110
+ end
111
+
112
+ # Wraps an explicitly reviewed dynamic SQL escape hatch.
113
+ #
114
+ # @param sql [String, #to_s] SQL that cannot be represented with placeholders.
115
+ # @param reason [String] Durable explanation for why raw SQL is safe here.
116
+ # @return [ClickHouse::SQL::Raw] A raw SQL expression with an audit reason.
117
+ def unsafe_raw(sql, reason:)
118
+ raise Error, "unsafe_raw requires a reason" if ValueNormalization.blank_string?(reason)
119
+
120
+ Raw.new(sql, unsafe_reason: reason)
121
+ end
122
+
123
+ # Builds a validated table handle that qualifies column references.
124
+ #
125
+ # @param name [String, Symbol, #to_s] Trusted table name.
126
+ # @param as [String, Symbol, nil] Optional table alias.
127
+ # @return [ClickHouse::SQL::Table] A validated table handle.
128
+ def table(name, as: nil)
129
+ Table.new(name, as:)
130
+ end
131
+
132
+ # Builds a standalone JOIN clause for SelectQuery#joins.
133
+ #
134
+ # Useful for join specs shared between queries, so the joined table, its
135
+ # alias, FINAL, and the ON conditions travel together.
136
+ #
137
+ # @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
138
+ # @param type [String] JOIN type, for example `INNER` or `LEFT`.
139
+ # @param as [String, Symbol, nil] Optional table alias; only valid for
140
+ # string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
141
+ # @param final [Boolean] Whether to append ClickHouse `FINAL`.
142
+ # @param on [String, ClickHouse::SQL::Part, Array] ON conditions.
143
+ # @return [ClickHouse::SQL::Join] A validated JOIN clause.
144
+ def join(table, type: "INNER", as: nil, final: false, on:)
145
+ Join.new(table, type:, as:, final:, on:)
146
+ end
147
+
148
+ # Builds a standalone LEFT JOIN clause for SelectQuery#joins.
149
+ #
150
+ # @param table [String, Symbol, ClickHouse::SQL::Part] Trusted table expression.
151
+ # @param as [String, Symbol, nil] Optional table alias; only valid for
152
+ # string/symbol table names — a ClickHouse::SQL::Table handle owns its alias.
153
+ # @param final [Boolean] Whether to append ClickHouse `FINAL`.
154
+ # @param on [String, ClickHouse::SQL::Part, Array] ON conditions.
155
+ # @return [ClickHouse::SQL::Join] A validated LEFT JOIN clause.
156
+ def left_join(table, as: nil, final: false, on:)
157
+ join(table, type: "LEFT", as:, final:, on:)
158
+ end
159
+
160
+ # Returns a trusted `*` select expression.
161
+ #
162
+ # @return [ClickHouse::SQL::Raw] A raw `*` expression.
163
+ def star
164
+ raw_static("*")
165
+ end
166
+
167
+ # Starts a mutable SELECT query.
168
+ #
169
+ # @param columns [Array<String, Symbol, ClickHouse::SQL::Part>] SELECT expressions.
170
+ # @param aliases [Hash{Symbol,String=>String,Symbol,ClickHouse::SQL::Part}] Aliased expressions.
171
+ # @return [ClickHouse::SQL::SelectQuery] A chainable SELECT query.
172
+ def select(*columns, **aliases)
173
+ SelectQuery.new.select(*columns, **aliases)
174
+ end
175
+
176
+ # Joins present fragments with `AND`, parenthesizing each fragment so a
177
+ # fragment containing a top-level `OR` cannot change the clause's meaning.
178
+ #
179
+ # @param fragments [Array<String, Symbol, ClickHouse::SQL::Part, nil>] SQL fragments to join.
180
+ # @return [ClickHouse::SQL::Fragment, nil] Joined fragment, or nil when no fragments are present.
181
+ def and(fragments)
182
+ join_fragments(fragments, " AND ", wrap: true)
183
+ end
184
+
185
+ # Joins present fragments with `OR`, parenthesizing each fragment so a
186
+ # fragment containing a top-level `AND` cannot change the clause's meaning.
187
+ #
188
+ # @param fragments [Array<String, Symbol, ClickHouse::SQL::Part, nil>] SQL fragments to join.
189
+ # @return [ClickHouse::SQL::Fragment, nil] Joined fragment, or nil when no fragments are present.
190
+ def or(fragments)
191
+ join_fragments(fragments, " OR ", wrap: true)
192
+ end
193
+
194
+ # Joins present fragments with `, `.
195
+ #
196
+ # @param fragments [Array<String, Symbol, ClickHouse::SQL::Part, nil>] SQL fragments to join.
197
+ # @return [ClickHouse::SQL::Fragment, nil] Joined fragment, or nil when no fragments are present.
198
+ def csv(fragments)
199
+ join_fragments(fragments, ", ")
200
+ end
201
+
202
+ # Wraps a fragment in parentheses.
203
+ #
204
+ # @param fragment [String, Symbol, ClickHouse::SQL::Part] SQL expression to wrap.
205
+ # @return [ClickHouse::SQL::Fragment] Parenthesized SQL fragment.
206
+ def parens(fragment)
207
+ Fragment.new("({})", fills: [coerce_expression(fragment)])
208
+ end
209
+
210
+ # Builds a comma-separated list of generated typed placeholders.
211
+ #
212
+ # @param prefix [String, Symbol] Safe prefix for generated placeholder names.
213
+ # @param values [Array, Object] Values to bind; non-arrays are wrapped.
214
+ # @param type [String] ClickHouse placeholder type for every value.
215
+ # @param compact [Boolean] Whether to use shorter base-36 placeholder suffixes.
216
+ # @return [ClickHouse::SQL::Fragment] Comma-separated placeholder fragment.
217
+ def bind_list(prefix, values, type:, compact: false)
218
+ unless prefix.to_s.match?(GENERATED_BIND_PREFIX_REGEX)
219
+ raise Error, "Generated bind prefix must be a simple identifier: #{prefix.inspect}"
220
+ end
221
+
222
+ unless type.to_s.match?(GENERATED_BIND_TYPE_REGEX)
223
+ raise Error, "Generated bind type must not be blank or contain braces: #{type.inspect}"
224
+ end
225
+
226
+ values = Array(values)
227
+ raise Error, "bind_list requires at least one value" if values.empty?
228
+
229
+ bindings = {}
230
+ placeholders = values.each_with_index.map do |value, index|
231
+ name = generated_bind_name(prefix, index, compact:)
232
+ bindings[name.to_sym] = value
233
+ "{#{name}:#{type}}"
234
+ end
235
+
236
+ Fragment.new(placeholders.join(", "), bindings)
237
+ end
238
+
239
+ # Builds a validated ClickHouse SETTINGS assignment.
240
+ #
241
+ # @param name [String, Symbol] ClickHouse setting name.
242
+ # @param value [String, Integer, Float, Boolean] Inline setting value.
243
+ # @return [ClickHouse::SQL::Setting] A rendered setting expression.
244
+ def setting(name, value)
245
+ Setting.new(name, value)
246
+ end
247
+
248
+ # Parses ClickHouse placeholder markers from SQL text.
249
+ #
250
+ # @param sql [String, #to_s] SQL text containing `{name}` or `{name:Type}` markers.
251
+ # @return [Array<ClickHouse::SQL::Placeholder>] Parsed placeholders in source order.
252
+ def parse_placeholders(sql)
253
+ sql = sql.to_s
254
+ placeholders = []
255
+ index = 0
256
+
257
+ while (open_pos = sql.index("{", index))
258
+ close_pos = sql.index("}", open_pos + 1)
259
+ raise Error, "Unclosed ClickHouse placeholder in SQL fragment" unless close_pos
260
+
261
+ raw = sql[open_pos..close_pos]
262
+ content = raw[1...-1]
263
+ if ValueNormalization.blank_string?(content)
264
+ # `{}` is an anonymous fragment slot, filled positionally.
265
+ placeholders << Placeholder.new(
266
+ raw:,
267
+ name: nil,
268
+ type: nil,
269
+ begin_pos: open_pos,
270
+ end_pos: close_pos + 1
271
+ )
272
+ index = close_pos + 1
273
+ next
274
+ end
275
+
276
+ # A colon must be followed by a non-empty type; `{name:}` is invalid
277
+ # rather than silently becoming an untyped fragment slot.
278
+ match = content.match(/\A\s*([A-Za-z_]\w*)\s*(?::\s*(\S.*?)\s*)?\z/m)
279
+ raise Error, "Invalid ClickHouse placeholder #{raw.inspect}" unless match
280
+
281
+ placeholders << Placeholder.new(
282
+ raw:,
283
+ name: match[1],
284
+ type: match[2]&.strip,
285
+ begin_pos: open_pos,
286
+ end_pos: close_pos + 1
287
+ )
288
+
289
+ index = close_pos + 1
290
+ end
291
+
292
+ placeholders
293
+ end
294
+
295
+ # Validates a simple SQL alias.
296
+ #
297
+ # @param alias_name [String, Symbol, #to_s] Alias to validate.
298
+ # @return [String] The validated alias string.
299
+ def validate_alias!(alias_name)
300
+ alias_name = alias_name.to_s
301
+ return alias_name if alias_name.match?(SQL_ALIAS_REGEX)
302
+
303
+ raise Error, "SQL alias must be a simple identifier: #{alias_name.inspect}"
304
+ end
305
+
306
+ # Resolves a clause alias against a table expression's own alias.
307
+ #
308
+ # A ClickHouse::SQL::Table handle owns its alias so qualified column
309
+ # references built from it always match the rendered FROM/JOIN clause.
310
+ # An explicit clause alias alongside a handle — even one matching the
311
+ # handle's alias — would create a second source of truth that can drift,
312
+ # so it raises instead.
313
+ #
314
+ # @param table [Object] Table expression, possibly a ClickHouse::SQL::Table.
315
+ # @param as [String, Symbol, nil] Explicit clause alias, if any.
316
+ # @return [String, Symbol, nil] The alias to render, if any.
317
+ def resolve_table_alias(table, as)
318
+ return as unless table.is_a?(ClickHouse::SQL::Table)
319
+
320
+ unless as.nil?
321
+ raise Error, "A table handle owns its alias; pass the alias when creating the handle: " \
322
+ "ClickHouse::SQL.table(#{table.name.inspect}, as: #{as.to_s.inspect})"
323
+ end
324
+
325
+ table.alias_name
326
+ end
327
+
328
+ # Validates a ClickHouse setting name.
329
+ #
330
+ # @param name [String, Symbol, #to_s] Setting name to validate.
331
+ # @return [String] The validated setting name.
332
+ def validate_setting_name!(name)
333
+ name = name.to_s
334
+ return name if name.match?(SETTING_NAME_REGEX)
335
+
336
+ raise Error, "ClickHouse setting name must be a simple identifier: #{name.inspect}"
337
+ end
338
+
339
+ # Coerces a value into a SQL expression that may contain placeholders.
340
+ #
341
+ # @param value [String, Symbol, ClickHouse::SQL::Part, nil] Expression value.
342
+ # @return [ClickHouse::SQL::Part, nil] Coerced SQL expression.
343
+ def coerce_expression(value)
344
+ case value
345
+ when nil
346
+ nil
347
+ when ClickHouse::SQL::Part
348
+ value
349
+ when String
350
+ Fragment.new(value)
351
+ when Symbol
352
+ Raw.new(value.to_s)
353
+ else
354
+ raise Error, "Expected a SQL fragment or trusted SQL string, got #{value.class}"
355
+ end
356
+ end
357
+
358
+ # Coerces a value into trusted raw SQL expression context.
359
+ #
360
+ # @param value [String, Symbol, ClickHouse::SQL::Part, nil] Raw expression value.
361
+ # @return [ClickHouse::SQL::Part, nil] Coerced raw SQL expression.
362
+ def coerce_raw_expression(value)
363
+ case value
364
+ when nil
365
+ nil
366
+ when ClickHouse::SQL::Part
367
+ value
368
+ when String
369
+ Raw.new(value)
370
+ when Symbol
371
+ Raw.new(value.to_s)
372
+ else
373
+ raise Error, "Expected a trusted SQL string, got #{value.class}"
374
+ end
375
+ end
376
+
377
+ # Inspects a value for an error message without embedding whole
378
+ # user-supplied values, which would otherwise travel into error trackers
379
+ # when a caller bug raises.
380
+ #
381
+ # @param value [Object] Value to render into an error message.
382
+ # @param limit [Integer] Maximum inspected length kept verbatim.
383
+ # @return [String] Inspect output, truncated when oversized.
384
+ def truncated_inspect(value, limit: 32)
385
+ inspected = value.inspect
386
+ return inspected if inspected.length <= limit
387
+
388
+ "#{inspected[0, limit]}… (truncated #{value.class}, #{inspected.length} chars)"
389
+ end
390
+
391
+ # Merges placeholder values, rejecting duplicate names with different values.
392
+ #
393
+ # @param target [Hash{Symbol=>Object}] Placeholder values to mutate.
394
+ # @param source [Hash{Symbol,String=>Object}] Placeholder values to merge in.
395
+ # @return [Hash{Symbol=>Object}] The mutated target hash.
396
+ def merge_placeholders!(target, source)
397
+ source.each do |key, value|
398
+ key = key.to_sym
399
+ if target.key?(key) && target[key] != value
400
+ raise Error, "mismatching values for the '#{key}' placeholder: " \
401
+ "#{truncated_inspect(target[key])} vs #{truncated_inspect(value)}"
402
+ end
403
+
404
+ target[key] = value
405
+ end
406
+
407
+ target
408
+ end
409
+
410
+ # Merges placeholder types, rejecting duplicate names with different types.
411
+ #
412
+ # @param target [Hash{Symbol=>String}] Placeholder types to mutate.
413
+ # @param source [Hash{Symbol,String=>String}] Placeholder types to merge in.
414
+ # @return [Hash{Symbol=>String}] The mutated target hash.
415
+ def merge_placeholder_types!(target, source)
416
+ source.each do |key, type|
417
+ key = key.to_sym
418
+ if target.key?(key) && target[key] != type
419
+ raise Error, "mismatching types for the '#{key}' placeholder: #{target[key]} vs #{type}"
420
+ end
421
+
422
+ target[key] = type
423
+ end
424
+
425
+ target
426
+ end
427
+
428
+ # Normalizes placeholder type spelling for comparison.
429
+ #
430
+ # @param type [String, #to_s] ClickHouse placeholder type.
431
+ # @return [String] Type with whitespace removed.
432
+ def normalize_placeholder_type(type)
433
+ type.to_s.gsub(/\s+/, "")
434
+ end
435
+
436
+ # Renders a query part in normal or redacted mode.
437
+ #
438
+ # @param part [ClickHouse::SQL::Part] Query part to render.
439
+ # @param redacted [Boolean] Whether to render typed placeholders as `?`-style bind markers.
440
+ # @param bind_index_manager [ClickHouse::SQL::BindIndexManager] Redacted bind index state.
441
+ # @return [String] Rendered SQL.
442
+ def render_part(part, redacted:, bind_index_manager:)
443
+ if redacted
444
+ part.to_redacted_sql(bind_index_manager)
445
+ else
446
+ part.to_sql
447
+ end
448
+ end
449
+
450
+ # Collects placeholder values from query parts.
451
+ #
452
+ # @param parts [Array<#placeholders, nil>] Query parts to inspect.
453
+ # @return [Hash{Symbol=>Object}] Merged placeholder values.
454
+ def collect_placeholders(parts)
455
+ parts.compact.each_with_object({}) do |part, placeholders|
456
+ next unless part.respond_to?(:placeholders)
457
+
458
+ merge_placeholders!(placeholders, part.placeholders)
459
+ end
460
+ end
461
+
462
+ # Collects placeholder types from query parts.
463
+ #
464
+ # @param parts [Array<#placeholder_types, nil>] Query parts to inspect.
465
+ # @return [Hash{Symbol=>String}] Merged placeholder type names.
466
+ def collect_placeholder_types(parts)
467
+ parts.compact.each_with_object({}) do |part, placeholder_types|
468
+ next unless part.respond_to?(:placeholder_types)
469
+
470
+ merge_placeholder_types!(placeholder_types, part.placeholder_types)
471
+ end
472
+ end
473
+
474
+ # Splits a ClickHouse type argument list on top-level commas.
475
+ #
476
+ # @param input [String, #to_s] Argument list without the outer type name.
477
+ # @return [Array<String>] Top-level argument strings.
478
+ def split_top_level_arguments(input)
479
+ input = input.to_s
480
+ arguments = []
481
+ start = 0
482
+ depth = 0
483
+ quote = nil
484
+ escaped = false
485
+
486
+ input.each_char.with_index do |char, index|
487
+ if quote
488
+ # An escaped character never closes the string; a backslash that is
489
+ # not itself escaped escapes the character after it.
490
+ if escaped
491
+ escaped = false
492
+ elsif char == "\\"
493
+ escaped = true
494
+ elsif char == quote
495
+ quote = nil
496
+ end
497
+ next
498
+ end
499
+
500
+ case char
501
+ when "'", '"'
502
+ quote = char
503
+ when "("
504
+ depth += 1
505
+ when ")"
506
+ depth -= 1 if depth.positive?
507
+ when ","
508
+ next unless depth.zero?
509
+
510
+ arguments << input[start...index].strip
511
+ start = index + 1
512
+ end
513
+ end
514
+
515
+ arguments << input[start..].to_s.strip
516
+ arguments
517
+ end
518
+
519
+ # Merges positional and keyword bindings for a fragment.
520
+ #
521
+ # @param bindings [Hash, nil] Explicit bindings hash.
522
+ # @param keyword_bindings [Hash] Keyword bindings hash.
523
+ # @return [Hash{Symbol=>Object}] Symbol-keyed merged bindings.
524
+ def merge_bindings(bindings, keyword_bindings)
525
+ ValueNormalization.symbolize_keys((bindings || {}).merge(keyword_bindings))
526
+ end
527
+
528
+ # Builds a generated placeholder name.
529
+ #
530
+ # @param prefix [String, Symbol, #to_s] Placeholder name prefix.
531
+ # @param index [Integer] Zero-based generated placeholder index.
532
+ # @param compact [Boolean] Whether to use base-36 suffixes.
533
+ # @return [String] Generated placeholder name.
534
+ def generated_bind_name(prefix, index, compact:)
535
+ return "#{prefix}_#{index}" unless compact
536
+
537
+ "#{prefix}#{index.to_s(36)}"
538
+ end
539
+
540
+ # Returns whether a value is a renderable SQL builder part.
541
+ #
542
+ # @param value [Object] Value to check.
543
+ # @return [Boolean] True when value implements ClickHouse::SQL's internal part protocol.
544
+ def part?(value)
545
+ value.is_a?(ClickHouse::SQL::Part)
546
+ end
547
+
548
+ # Joins fragments with a separator while preserving nested placeholders.
549
+ #
550
+ # @param fragments [Array<String, Symbol, ClickHouse::SQL::Part, nil>] Fragments to join.
551
+ # @param separator [String] SQL separator to place between fragments.
552
+ # @param wrap [Boolean] Whether to parenthesize each fragment, for boolean separators.
553
+ # @return [ClickHouse::SQL::Fragment, nil] Joined fragment, or nil when empty.
554
+ def join_fragments(fragments, separator, wrap: false)
555
+ fills = Array(fragments).flatten.compact.map { |fragment| coerce_expression(fragment) }
556
+ return nil if fills.empty?
557
+
558
+ slots = fills.map { wrap ? "({})" : "{}" }
559
+ Fragment.new(slots.join(separator), fills:)
560
+ end
561
+ end
562
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clickhouse/sql/version"
4
+ require_relative "clickhouse/sql"
5
+ require_relative "clickhouse/http_client"
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clickhouse-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Buildkite
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '8.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '13.4'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '13.4'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.13'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.13'
60
+ - !ruby/object:Gem::Dependency
61
+ name: webmock
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.26'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3.26'
74
+ description: Build composable ClickHouse SELECT queries with typed placeholders and
75
+ execute them over HTTP.
76
+ email:
77
+ - paul@buildkite.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - CHANGELOG.md
83
+ - LICENSE
84
+ - README.md
85
+ - lib/clickhouse-sql.rb
86
+ - lib/clickhouse/http_client.rb
87
+ - lib/clickhouse/http_client/database.rb
88
+ - lib/clickhouse/http_client/error.rb
89
+ - lib/clickhouse/http_client/parameter_serializer.rb
90
+ - lib/clickhouse/http_client/query.rb
91
+ - lib/clickhouse/http_client/response_formatter.rb
92
+ - lib/clickhouse/http_client/value_normalization.rb
93
+ - lib/clickhouse/sql.rb
94
+ - lib/clickhouse/sql/bind_index_manager.rb
95
+ - lib/clickhouse/sql/error.rb
96
+ - lib/clickhouse/sql/fragment.rb
97
+ - lib/clickhouse/sql/join.rb
98
+ - lib/clickhouse/sql/part.rb
99
+ - lib/clickhouse/sql/placeholder.rb
100
+ - lib/clickhouse/sql/placeholder_type.rb
101
+ - lib/clickhouse/sql/query.rb
102
+ - lib/clickhouse/sql/raw.rb
103
+ - lib/clickhouse/sql/select_item.rb
104
+ - lib/clickhouse/sql/select_query.rb
105
+ - lib/clickhouse/sql/setting.rb
106
+ - lib/clickhouse/sql/table.rb
107
+ - lib/clickhouse/sql/version.rb
108
+ homepage: https://github.com/buildkite/clickhouse-sql
109
+ licenses:
110
+ - MIT
111
+ metadata:
112
+ homepage_uri: https://github.com/buildkite/clickhouse-sql
113
+ source_code_uri: https://github.com/buildkite/clickhouse-sql/tree/main
114
+ changelog_uri: https://github.com/buildkite/clickhouse-sql/blob/main/CHANGELOG.md
115
+ rubygems_mfa_required: 'true'
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '3.4'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubygems_version: 3.6.9
131
+ specification_version: 4
132
+ summary: A typed ClickHouse SQL query builder and HTTP transport client
133
+ test_files: []