clickhouse-ruby 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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE +21 -0
  4. data/README.md +251 -0
  5. data/lib/clickhouse_ruby/active_record/arel_visitor.rb +468 -0
  6. data/lib/clickhouse_ruby/active_record/connection_adapter.rb +723 -0
  7. data/lib/clickhouse_ruby/active_record/railtie.rb +192 -0
  8. data/lib/clickhouse_ruby/active_record/schema_statements.rb +693 -0
  9. data/lib/clickhouse_ruby/active_record.rb +121 -0
  10. data/lib/clickhouse_ruby/client.rb +471 -0
  11. data/lib/clickhouse_ruby/configuration.rb +145 -0
  12. data/lib/clickhouse_ruby/connection.rb +328 -0
  13. data/lib/clickhouse_ruby/connection_pool.rb +301 -0
  14. data/lib/clickhouse_ruby/errors.rb +144 -0
  15. data/lib/clickhouse_ruby/result.rb +189 -0
  16. data/lib/clickhouse_ruby/types/array.rb +183 -0
  17. data/lib/clickhouse_ruby/types/base.rb +77 -0
  18. data/lib/clickhouse_ruby/types/boolean.rb +68 -0
  19. data/lib/clickhouse_ruby/types/date_time.rb +163 -0
  20. data/lib/clickhouse_ruby/types/float.rb +115 -0
  21. data/lib/clickhouse_ruby/types/integer.rb +157 -0
  22. data/lib/clickhouse_ruby/types/low_cardinality.rb +58 -0
  23. data/lib/clickhouse_ruby/types/map.rb +249 -0
  24. data/lib/clickhouse_ruby/types/nullable.rb +73 -0
  25. data/lib/clickhouse_ruby/types/parser.rb +244 -0
  26. data/lib/clickhouse_ruby/types/registry.rb +148 -0
  27. data/lib/clickhouse_ruby/types/string.rb +83 -0
  28. data/lib/clickhouse_ruby/types/tuple.rb +206 -0
  29. data/lib/clickhouse_ruby/types/uuid.rb +84 -0
  30. data/lib/clickhouse_ruby/types.rb +69 -0
  31. data/lib/clickhouse_ruby/version.rb +5 -0
  32. data/lib/clickhouse_ruby.rb +101 -0
  33. metadata +150 -0
@@ -0,0 +1,468 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arel/visitors/to_sql'
4
+
5
+ module ClickhouseRuby
6
+ module ActiveRecord
7
+ # Custom Arel visitor for generating ClickHouse-specific SQL
8
+ #
9
+ # ClickHouse has unique requirements for certain SQL operations:
10
+ # - DELETE: Uses ALTER TABLE ... DELETE WHERE syntax
11
+ # - UPDATE: Uses ALTER TABLE ... UPDATE ... WHERE syntax
12
+ # - LIMIT: Must come after ORDER BY
13
+ # - No OFFSET without LIMIT (use LIMIT n, m syntax)
14
+ #
15
+ # @example DELETE conversion
16
+ # # Standard SQL: DELETE FROM events WHERE id = 1
17
+ # # ClickHouse: ALTER TABLE events DELETE WHERE id = 1
18
+ #
19
+ # @example UPDATE conversion
20
+ # # Standard SQL: UPDATE events SET status = 'done' WHERE id = 1
21
+ # # ClickHouse: ALTER TABLE events UPDATE status = 'done' WHERE id = 1
22
+ #
23
+ class ArelVisitor < ::Arel::Visitors::ToSql
24
+ # Initialize the visitor
25
+ #
26
+ # @param connection [ConnectionAdapter] the database connection
27
+ def initialize(connection)
28
+ super(connection)
29
+ @connection = connection
30
+ end
31
+
32
+ private
33
+
34
+ # Visit a DELETE statement
35
+ # Converts to ClickHouse ALTER TABLE ... DELETE WHERE syntax
36
+ #
37
+ # @param o [Arel::Nodes::DeleteStatement] the delete node
38
+ # @param collector [Arel::Collectors::SQLString] SQL collector
39
+ # @return [Arel::Collectors::SQLString] the collector with SQL
40
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
41
+ # Get table name
42
+ table = o.relation
43
+
44
+ # Build ClickHouse DELETE syntax
45
+ collector << 'ALTER TABLE '
46
+ collector = visit(table, collector)
47
+ collector << ' DELETE'
48
+
49
+ # Add WHERE clause (required for ClickHouse DELETE)
50
+ if o.wheres.any?
51
+ collector << ' WHERE '
52
+ collector = inject_join(o.wheres, collector, ' AND ')
53
+ else
54
+ # ClickHouse requires WHERE clause for DELETE
55
+ # Use 1=1 to delete all rows
56
+ collector << ' WHERE 1=1'
57
+ end
58
+
59
+ collector
60
+ end
61
+
62
+ # Visit an UPDATE statement
63
+ # Converts to ClickHouse ALTER TABLE ... UPDATE ... WHERE syntax
64
+ #
65
+ # @param o [Arel::Nodes::UpdateStatement] the update node
66
+ # @param collector [Arel::Collectors::SQLString] SQL collector
67
+ # @return [Arel::Collectors::SQLString] the collector with SQL
68
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
69
+ # Get table name
70
+ table = o.relation
71
+
72
+ # Build ClickHouse UPDATE syntax
73
+ collector << 'ALTER TABLE '
74
+ collector = visit(table, collector)
75
+ collector << ' UPDATE '
76
+
77
+ # Add SET assignments
78
+ unless o.values.empty?
79
+ collector = inject_join(o.values, collector, ', ')
80
+ end
81
+
82
+ # Add WHERE clause (required for ClickHouse UPDATE)
83
+ if o.wheres.any?
84
+ collector << ' WHERE '
85
+ collector = inject_join(o.wheres, collector, ' AND ')
86
+ else
87
+ # ClickHouse requires WHERE clause for UPDATE
88
+ collector << ' WHERE 1=1'
89
+ end
90
+
91
+ collector
92
+ end
93
+
94
+ # Visit a LIMIT node
95
+ # ClickHouse supports LIMIT with optional OFFSET
96
+ #
97
+ # @param o [Arel::Nodes::Limit] the limit node
98
+ # @param collector [Arel::Collectors::SQLString] SQL collector
99
+ # @return [Arel::Collectors::SQLString] the collector with SQL
100
+ def visit_Arel_Nodes_Limit(o, collector)
101
+ collector << 'LIMIT '
102
+ visit(o.expr, collector)
103
+ end
104
+
105
+ # Visit an OFFSET node
106
+ # ClickHouse uses OFFSET after LIMIT (LIMIT n OFFSET m)
107
+ #
108
+ # @param o [Arel::Nodes::Offset] the offset node
109
+ # @param collector [Arel::Collectors::SQLString] SQL collector
110
+ # @return [Arel::Collectors::SQLString] the collector with SQL
111
+ def visit_Arel_Nodes_Offset(o, collector)
112
+ collector << 'OFFSET '
113
+ visit(o.expr, collector)
114
+ end
115
+
116
+ # Visit a SelectStatement
117
+ # Ensures proper ordering of clauses for ClickHouse
118
+ #
119
+ # @param o [Arel::Nodes::SelectStatement] the select node
120
+ # @param collector [Arel::Collectors::SQLString] SQL collector
121
+ # @return [Arel::Collectors::SQLString] the collector with SQL
122
+ def visit_Arel_Nodes_SelectStatement(o, collector)
123
+ # Use default behavior but ensure ClickHouse compatibility
124
+ super
125
+ end
126
+
127
+ # Visit a table alias
128
+ # ClickHouse uses AS keyword for table aliases
129
+ #
130
+ # @param o [Arel::Nodes::TableAlias] the table alias node
131
+ # @param collector [Arel::Collectors::SQLString] SQL collector
132
+ # @return [Arel::Collectors::SQLString] the collector with SQL
133
+ def visit_Arel_Nodes_TableAlias(o, collector)
134
+ collector = visit(o.relation, collector)
135
+ collector << ' AS '
136
+ collector << quote_table_name(o.name)
137
+ end
138
+
139
+ # Quote a table name using the connection's quoting
140
+ #
141
+ # @param name [String] the table name
142
+ # @return [String] the quoted table name
143
+ def quote_table_name(name)
144
+ @connection.quote_table_name(name)
145
+ end
146
+
147
+ # Quote a column name using the connection's quoting
148
+ #
149
+ # @param name [String] the column name
150
+ # @return [String] the quoted column name
151
+ def quote_column_name(name)
152
+ @connection.quote_column_name(name)
153
+ end
154
+
155
+ # Visit a True node
156
+ #
157
+ # @param o [Arel::Nodes::True] the true node
158
+ # @param collector [Arel::Collectors::SQLString] SQL collector
159
+ # @return [Arel::Collectors::SQLString] the collector with SQL
160
+ def visit_Arel_Nodes_True(o, collector)
161
+ collector << '1'
162
+ end
163
+
164
+ # Visit a False node
165
+ #
166
+ # @param o [Arel::Nodes::False] the false node
167
+ # @param collector [Arel::Collectors::SQLString] SQL collector
168
+ # @return [Arel::Collectors::SQLString] the collector with SQL
169
+ def visit_Arel_Nodes_False(o, collector)
170
+ collector << '0'
171
+ end
172
+
173
+ # Visit a CASE statement
174
+ #
175
+ # @param o [Arel::Nodes::Case] the case node
176
+ # @param collector [Arel::Collectors::SQLString] SQL collector
177
+ # @return [Arel::Collectors::SQLString] the collector with SQL
178
+ def visit_Arel_Nodes_Case(o, collector)
179
+ collector << 'CASE '
180
+
181
+ if o.case
182
+ visit(o.case, collector)
183
+ collector << ' '
184
+ end
185
+
186
+ o.conditions.each do |condition|
187
+ visit(condition, collector)
188
+ collector << ' '
189
+ end
190
+
191
+ if o.default
192
+ collector << 'ELSE '
193
+ visit(o.default, collector)
194
+ collector << ' '
195
+ end
196
+
197
+ collector << 'END'
198
+ end
199
+
200
+ # Handle INSERT statements
201
+ # ClickHouse uses standard INSERT syntax but with some differences
202
+ #
203
+ # @param o [Arel::Nodes::InsertStatement] the insert node
204
+ # @param collector [Arel::Collectors::SQLString] SQL collector
205
+ # @return [Arel::Collectors::SQLString] the collector with SQL
206
+ def visit_Arel_Nodes_InsertStatement(o, collector)
207
+ collector << 'INSERT INTO '
208
+ collector = visit(o.relation, collector)
209
+
210
+ if o.columns.any?
211
+ collector << ' ('
212
+ o.columns.each_with_index do |column, i|
213
+ collector << ', ' if i > 0
214
+ collector << quote_column_name(column.name)
215
+ end
216
+ collector << ')'
217
+ end
218
+
219
+ if o.values
220
+ collector << ' VALUES '
221
+ collector = visit(o.values, collector)
222
+ elsif o.select
223
+ collector << ' '
224
+ collector = visit(o.select, collector)
225
+ end
226
+
227
+ collector
228
+ end
229
+
230
+ # Handle VALUES list
231
+ #
232
+ # @param o [Arel::Nodes::Values] the values node
233
+ # @param collector [Arel::Collectors::SQLString] SQL collector
234
+ # @return [Arel::Collectors::SQLString] the collector with SQL
235
+ def visit_Arel_Nodes_Values(o, collector)
236
+ collector << '('
237
+ o.expressions.each_with_index do |expr, i|
238
+ collector << ', ' if i > 0
239
+ case expr
240
+ when Arel::Nodes::SqlLiteral
241
+ collector << expr.to_s
242
+ when nil
243
+ collector << 'NULL'
244
+ else
245
+ collector = visit(expr, collector)
246
+ end
247
+ end
248
+ collector << ')'
249
+ end
250
+
251
+ # Handle multiple VALUES rows for bulk insert
252
+ #
253
+ # @param o [Arel::Nodes::ValuesList] the values list node
254
+ # @param collector [Arel::Collectors::SQLString] SQL collector
255
+ # @return [Arel::Collectors::SQLString] the collector with SQL
256
+ def visit_Arel_Nodes_ValuesList(o, collector)
257
+ o.rows.each_with_index do |row, i|
258
+ collector << ', ' if i > 0
259
+ collector << '('
260
+ row.each_with_index do |value, j|
261
+ collector << ', ' if j > 0
262
+ case value
263
+ when Arel::Nodes::SqlLiteral
264
+ collector << value.to_s
265
+ when nil
266
+ collector << 'NULL'
267
+ else
268
+ collector = visit(value, collector)
269
+ end
270
+ end
271
+ collector << ')'
272
+ end
273
+ collector
274
+ end
275
+
276
+ # Handle assignment for UPDATE statements
277
+ #
278
+ # @param o [Arel::Nodes::Assignment] the assignment node
279
+ # @param collector [Arel::Collectors::SQLString] SQL collector
280
+ # @return [Arel::Collectors::SQLString] the collector with SQL
281
+ def visit_Arel_Nodes_Assignment(o, collector)
282
+ case o.left
283
+ when Arel::Nodes::UnqualifiedColumn
284
+ collector << quote_column_name(o.left.name)
285
+ when Arel::Attributes::Attribute
286
+ collector << quote_column_name(o.left.name)
287
+ else
288
+ collector = visit(o.left, collector)
289
+ end
290
+
291
+ collector << ' = '
292
+
293
+ case o.right
294
+ when nil
295
+ collector << 'NULL'
296
+ else
297
+ collector = visit(o.right, collector)
298
+ end
299
+
300
+ collector
301
+ end
302
+
303
+ # Handle named functions
304
+ #
305
+ # @param o [Arel::Nodes::NamedFunction] the function node
306
+ # @param collector [Arel::Collectors::SQLString] SQL collector
307
+ # @return [Arel::Collectors::SQLString] the collector with SQL
308
+ def visit_Arel_Nodes_NamedFunction(o, collector)
309
+ collector << o.name
310
+ collector << '('
311
+
312
+ if o.distinct
313
+ collector << 'DISTINCT '
314
+ end
315
+
316
+ o.expressions.each_with_index do |expr, i|
317
+ collector << ', ' if i > 0
318
+ collector = visit(expr, collector)
319
+ end
320
+
321
+ collector << ')'
322
+
323
+ if o.alias
324
+ collector << ' AS '
325
+ collector << quote_column_name(o.alias)
326
+ end
327
+
328
+ collector
329
+ end
330
+
331
+ # Handle DISTINCT
332
+ #
333
+ # @param o [Arel::Nodes::Distinct] the distinct node
334
+ # @param collector [Arel::Collectors::SQLString] SQL collector
335
+ # @return [Arel::Collectors::SQLString] the collector with SQL
336
+ def visit_Arel_Nodes_Distinct(o, collector)
337
+ collector << 'DISTINCT'
338
+ end
339
+
340
+ # Handle GROUP BY
341
+ #
342
+ # @param o [Arel::Nodes::Group] the group node
343
+ # @param collector [Arel::Collectors::SQLString] SQL collector
344
+ # @return [Arel::Collectors::SQLString] the collector with SQL
345
+ def visit_Arel_Nodes_Group(o, collector)
346
+ visit(o.expr, collector)
347
+ end
348
+
349
+ # Handle HAVING
350
+ #
351
+ # @param o [Arel::Nodes::Having] the having node
352
+ # @param collector [Arel::Collectors::SQLString] SQL collector
353
+ # @return [Arel::Collectors::SQLString] the collector with SQL
354
+ def visit_Arel_Nodes_Having(o, collector)
355
+ collector << 'HAVING '
356
+ visit(o.expr, collector)
357
+ end
358
+
359
+ # Handle ordering (ASC/DESC)
360
+ #
361
+ # @param o [Arel::Nodes::Ordering] the ordering node
362
+ # @param collector [Arel::Collectors::SQLString] SQL collector
363
+ # @return [Arel::Collectors::SQLString] the collector with SQL
364
+ def visit_Arel_Nodes_Ascending(o, collector)
365
+ collector = visit(o.expr, collector)
366
+ collector << ' ASC'
367
+ end
368
+
369
+ # Handle descending order
370
+ #
371
+ # @param o [Arel::Nodes::Descending] the descending node
372
+ # @param collector [Arel::Collectors::SQLString] SQL collector
373
+ # @return [Arel::Collectors::SQLString] the collector with SQL
374
+ def visit_Arel_Nodes_Descending(o, collector)
375
+ collector = visit(o.expr, collector)
376
+ collector << ' DESC'
377
+ end
378
+
379
+ # Handle NULLS FIRST
380
+ #
381
+ # @param o [Arel::Nodes::NullsFirst] the nulls first node
382
+ # @param collector [Arel::Collectors::SQLString] SQL collector
383
+ # @return [Arel::Collectors::SQLString] the collector with SQL
384
+ def visit_Arel_Nodes_NullsFirst(o, collector)
385
+ collector = visit(o.expr, collector)
386
+ collector << ' NULLS FIRST'
387
+ end
388
+
389
+ # Handle NULLS LAST
390
+ #
391
+ # @param o [Arel::Nodes::NullsLast] the nulls last node
392
+ # @param collector [Arel::Collectors::SQLString] SQL collector
393
+ # @return [Arel::Collectors::SQLString] the collector with SQL
394
+ def visit_Arel_Nodes_NullsLast(o, collector)
395
+ collector = visit(o.expr, collector)
396
+ collector << ' NULLS LAST'
397
+ end
398
+
399
+ # Handle COUNT function
400
+ #
401
+ # @param o [Arel::Nodes::Count] the count node
402
+ # @param collector [Arel::Collectors::SQLString] SQL collector
403
+ # @return [Arel::Collectors::SQLString] the collector with SQL
404
+ def visit_Arel_Nodes_Count(o, collector)
405
+ aggregate('count', o, collector)
406
+ end
407
+
408
+ # Handle SUM function
409
+ #
410
+ # @param o [Arel::Nodes::Sum] the sum node
411
+ # @param collector [Arel::Collectors::SQLString] SQL collector
412
+ # @return [Arel::Collectors::SQLString] the collector with SQL
413
+ def visit_Arel_Nodes_Sum(o, collector)
414
+ aggregate('sum', o, collector)
415
+ end
416
+
417
+ # Handle AVG function
418
+ #
419
+ # @param o [Arel::Nodes::Avg] the avg node
420
+ # @param collector [Arel::Collectors::SQLString] SQL collector
421
+ # @return [Arel::Collectors::SQLString] the collector with SQL
422
+ def visit_Arel_Nodes_Avg(o, collector)
423
+ aggregate('avg', o, collector)
424
+ end
425
+
426
+ # Handle MIN function
427
+ #
428
+ # @param o [Arel::Nodes::Min] the min node
429
+ # @param collector [Arel::Collectors::SQLString] SQL collector
430
+ # @return [Arel::Collectors::SQLString] the collector with SQL
431
+ def visit_Arel_Nodes_Min(o, collector)
432
+ aggregate('min', o, collector)
433
+ end
434
+
435
+ # Handle MAX function
436
+ #
437
+ # @param o [Arel::Nodes::Max] the max node
438
+ # @param collector [Arel::Collectors::SQLString] SQL collector
439
+ # @return [Arel::Collectors::SQLString] the collector with SQL
440
+ def visit_Arel_Nodes_Max(o, collector)
441
+ aggregate('max', o, collector)
442
+ end
443
+
444
+ # Helper to generate aggregate functions
445
+ #
446
+ # @param name [String] function name
447
+ # @param o [Object] the node
448
+ # @param collector [Arel::Collectors::SQLString] SQL collector
449
+ # @return [Arel::Collectors::SQLString] the collector with SQL
450
+ def aggregate(name, o, collector)
451
+ collector << "#{name}("
452
+ if o.distinct
453
+ collector << 'DISTINCT '
454
+ end
455
+ o.expressions.each_with_index do |expr, i|
456
+ collector << ', ' if i > 0
457
+ collector = visit(expr, collector)
458
+ end
459
+ collector << ')'
460
+ if o.alias
461
+ collector << ' AS '
462
+ collector << quote_column_name(o.alias)
463
+ end
464
+ collector
465
+ end
466
+ end
467
+ end
468
+ end