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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE +21 -0
- data/README.md +251 -0
- data/lib/clickhouse_ruby/active_record/arel_visitor.rb +468 -0
- data/lib/clickhouse_ruby/active_record/connection_adapter.rb +723 -0
- data/lib/clickhouse_ruby/active_record/railtie.rb +192 -0
- data/lib/clickhouse_ruby/active_record/schema_statements.rb +693 -0
- data/lib/clickhouse_ruby/active_record.rb +121 -0
- data/lib/clickhouse_ruby/client.rb +471 -0
- data/lib/clickhouse_ruby/configuration.rb +145 -0
- data/lib/clickhouse_ruby/connection.rb +328 -0
- data/lib/clickhouse_ruby/connection_pool.rb +301 -0
- data/lib/clickhouse_ruby/errors.rb +144 -0
- data/lib/clickhouse_ruby/result.rb +189 -0
- data/lib/clickhouse_ruby/types/array.rb +183 -0
- data/lib/clickhouse_ruby/types/base.rb +77 -0
- data/lib/clickhouse_ruby/types/boolean.rb +68 -0
- data/lib/clickhouse_ruby/types/date_time.rb +163 -0
- data/lib/clickhouse_ruby/types/float.rb +115 -0
- data/lib/clickhouse_ruby/types/integer.rb +157 -0
- data/lib/clickhouse_ruby/types/low_cardinality.rb +58 -0
- data/lib/clickhouse_ruby/types/map.rb +249 -0
- data/lib/clickhouse_ruby/types/nullable.rb +73 -0
- data/lib/clickhouse_ruby/types/parser.rb +244 -0
- data/lib/clickhouse_ruby/types/registry.rb +148 -0
- data/lib/clickhouse_ruby/types/string.rb +83 -0
- data/lib/clickhouse_ruby/types/tuple.rb +206 -0
- data/lib/clickhouse_ruby/types/uuid.rb +84 -0
- data/lib/clickhouse_ruby/types.rb +69 -0
- data/lib/clickhouse_ruby/version.rb +5 -0
- data/lib/clickhouse_ruby.rb +101 -0
- metadata +150 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Schema introspection and manipulation methods for ClickHouse
|
|
6
|
+
#
|
|
7
|
+
# Provides methods to query and modify database schema through
|
|
8
|
+
# ClickHouse's system tables (system.tables, system.columns, etc.)
|
|
9
|
+
#
|
|
10
|
+
# @note ClickHouse schema operations differ significantly from traditional RDBMS:
|
|
11
|
+
# - Tables require ENGINE specification (MergeTree, etc.)
|
|
12
|
+
# - No SERIAL/AUTO_INCREMENT (use generateUUIDv4() or application-side IDs)
|
|
13
|
+
# - No ALTER TABLE ADD COLUMN migrations (use ALTER TABLE ADD COLUMN)
|
|
14
|
+
# - Indexes are defined at table creation time
|
|
15
|
+
#
|
|
16
|
+
module SchemaStatements
|
|
17
|
+
# Returns list of tables in the current database
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<String>] list of table names
|
|
20
|
+
def tables
|
|
21
|
+
sql = <<~SQL
|
|
22
|
+
SELECT name
|
|
23
|
+
FROM system.tables
|
|
24
|
+
WHERE database = currentDatabase()
|
|
25
|
+
AND engine NOT IN ('View', 'MaterializedView', 'LiveView')
|
|
26
|
+
ORDER BY name
|
|
27
|
+
SQL
|
|
28
|
+
|
|
29
|
+
result = execute(sql, 'SCHEMA')
|
|
30
|
+
result.map { |row| row['name'] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns list of views in the current database
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<String>] list of view names
|
|
36
|
+
def views
|
|
37
|
+
sql = <<~SQL
|
|
38
|
+
SELECT name
|
|
39
|
+
FROM system.tables
|
|
40
|
+
WHERE database = currentDatabase()
|
|
41
|
+
AND engine IN ('View', 'MaterializedView', 'LiveView')
|
|
42
|
+
ORDER BY name
|
|
43
|
+
SQL
|
|
44
|
+
|
|
45
|
+
result = execute(sql, 'SCHEMA')
|
|
46
|
+
result.map { |row| row['name'] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if a table exists
|
|
50
|
+
#
|
|
51
|
+
# @param table_name [String] the table name to check
|
|
52
|
+
# @return [Boolean] true if the table exists
|
|
53
|
+
def table_exists?(table_name)
|
|
54
|
+
sql = <<~SQL
|
|
55
|
+
SELECT 1
|
|
56
|
+
FROM system.tables
|
|
57
|
+
WHERE database = currentDatabase()
|
|
58
|
+
AND name = '#{quote_string(table_name.to_s)}'
|
|
59
|
+
LIMIT 1
|
|
60
|
+
SQL
|
|
61
|
+
|
|
62
|
+
result = execute(sql, 'SCHEMA')
|
|
63
|
+
result.any?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if a view exists
|
|
67
|
+
#
|
|
68
|
+
# @param view_name [String] the view name to check
|
|
69
|
+
# @return [Boolean] true if the view exists
|
|
70
|
+
def view_exists?(view_name)
|
|
71
|
+
sql = <<~SQL
|
|
72
|
+
SELECT 1
|
|
73
|
+
FROM system.tables
|
|
74
|
+
WHERE database = currentDatabase()
|
|
75
|
+
AND name = '#{quote_string(view_name.to_s)}'
|
|
76
|
+
AND engine IN ('View', 'MaterializedView', 'LiveView')
|
|
77
|
+
LIMIT 1
|
|
78
|
+
SQL
|
|
79
|
+
|
|
80
|
+
result = execute(sql, 'SCHEMA')
|
|
81
|
+
result.any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns list of indexes for a table
|
|
85
|
+
#
|
|
86
|
+
# @param table_name [String] the table name
|
|
87
|
+
# @return [Array<Hash>] list of index information
|
|
88
|
+
def indexes(table_name)
|
|
89
|
+
sql = <<~SQL
|
|
90
|
+
SELECT
|
|
91
|
+
name,
|
|
92
|
+
type,
|
|
93
|
+
expr,
|
|
94
|
+
granularity
|
|
95
|
+
FROM system.data_skipping_indices
|
|
96
|
+
WHERE database = currentDatabase()
|
|
97
|
+
AND table = '#{quote_string(table_name.to_s)}'
|
|
98
|
+
ORDER BY name
|
|
99
|
+
SQL
|
|
100
|
+
|
|
101
|
+
result = execute(sql, 'SCHEMA')
|
|
102
|
+
result.map do |row|
|
|
103
|
+
{
|
|
104
|
+
name: row['name'],
|
|
105
|
+
type: row['type'],
|
|
106
|
+
expression: row['expr'],
|
|
107
|
+
granularity: row['granularity']
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns list of columns for a table
|
|
113
|
+
#
|
|
114
|
+
# @param table_name [String] the table name
|
|
115
|
+
# @return [Array<Column>] list of column objects
|
|
116
|
+
def columns(table_name)
|
|
117
|
+
sql = <<~SQL
|
|
118
|
+
SELECT
|
|
119
|
+
name,
|
|
120
|
+
type,
|
|
121
|
+
default_kind,
|
|
122
|
+
default_expression,
|
|
123
|
+
comment,
|
|
124
|
+
is_in_primary_key,
|
|
125
|
+
is_in_sorting_key,
|
|
126
|
+
is_in_partition_key
|
|
127
|
+
FROM system.columns
|
|
128
|
+
WHERE database = currentDatabase()
|
|
129
|
+
AND table = '#{quote_string(table_name.to_s)}'
|
|
130
|
+
ORDER BY position
|
|
131
|
+
SQL
|
|
132
|
+
|
|
133
|
+
result = execute(sql, 'SCHEMA')
|
|
134
|
+
result.map do |row|
|
|
135
|
+
new_column(
|
|
136
|
+
row['name'],
|
|
137
|
+
row['default_expression'],
|
|
138
|
+
fetch_type_metadata(row['type']),
|
|
139
|
+
row['type'] =~ /^Nullable/i,
|
|
140
|
+
row['comment']
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns the primary key columns for a table
|
|
146
|
+
#
|
|
147
|
+
# @param table_name [String] the table name
|
|
148
|
+
# @return [Array<String>, nil] primary key column names or nil
|
|
149
|
+
def primary_keys(table_name)
|
|
150
|
+
sql = <<~SQL
|
|
151
|
+
SELECT name
|
|
152
|
+
FROM system.columns
|
|
153
|
+
WHERE database = currentDatabase()
|
|
154
|
+
AND table = '#{quote_string(table_name.to_s)}'
|
|
155
|
+
AND is_in_primary_key = 1
|
|
156
|
+
ORDER BY position
|
|
157
|
+
SQL
|
|
158
|
+
|
|
159
|
+
result = execute(sql, 'SCHEMA')
|
|
160
|
+
keys = result.map { |row| row['name'] }
|
|
161
|
+
keys.empty? ? nil : keys
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Create a new table
|
|
165
|
+
#
|
|
166
|
+
# @param table_name [String] the table name
|
|
167
|
+
# @param options [Hash] table options
|
|
168
|
+
# @option options [String] :engine the table engine (default: MergeTree)
|
|
169
|
+
# @option options [String] :order_by ORDER BY clause for MergeTree
|
|
170
|
+
# @option options [String] :partition_by PARTITION BY clause
|
|
171
|
+
# @option options [String] :primary_key PRIMARY KEY clause
|
|
172
|
+
# @option options [String] :settings table SETTINGS
|
|
173
|
+
# @yield [TableDefinition] the table definition block
|
|
174
|
+
# @return [void]
|
|
175
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
176
|
+
def create_table(table_name, **options, &block)
|
|
177
|
+
td = create_table_definition(table_name, **options)
|
|
178
|
+
|
|
179
|
+
if block_given?
|
|
180
|
+
yield td
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
sql = schema_creation.accept(td)
|
|
184
|
+
execute(sql, 'CREATE TABLE')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Drop a table
|
|
188
|
+
#
|
|
189
|
+
# @param table_name [String] the table name
|
|
190
|
+
# @param options [Hash] drop options
|
|
191
|
+
# @option options [Boolean] :if_exists add IF EXISTS clause
|
|
192
|
+
# @return [void]
|
|
193
|
+
# @raise [ClickhouseRuby::QueryError] on error (unless if_exists: true)
|
|
194
|
+
def drop_table(table_name, **options)
|
|
195
|
+
if_exists = options.fetch(:if_exists, false)
|
|
196
|
+
sql = "DROP TABLE #{if_exists ? 'IF EXISTS ' : ''}#{quote_table_name(table_name)}"
|
|
197
|
+
execute(sql, 'DROP TABLE')
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Rename a table
|
|
201
|
+
#
|
|
202
|
+
# @param old_name [String] the current table name
|
|
203
|
+
# @param new_name [String] the new table name
|
|
204
|
+
# @return [void]
|
|
205
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
206
|
+
def rename_table(old_name, new_name)
|
|
207
|
+
sql = "RENAME TABLE #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
|
|
208
|
+
execute(sql, 'RENAME TABLE')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Truncate a table (delete all data)
|
|
212
|
+
#
|
|
213
|
+
# @param table_name [String] the table name
|
|
214
|
+
# @param options [Hash] truncate options
|
|
215
|
+
# @return [void]
|
|
216
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
217
|
+
def truncate_table(table_name, **options)
|
|
218
|
+
sql = "TRUNCATE TABLE #{quote_table_name(table_name)}"
|
|
219
|
+
execute(sql, 'TRUNCATE TABLE')
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Add a column to a table
|
|
223
|
+
#
|
|
224
|
+
# @param table_name [String] the table name
|
|
225
|
+
# @param column_name [String] the column name
|
|
226
|
+
# @param type [Symbol, String] the column type
|
|
227
|
+
# @param options [Hash] column options
|
|
228
|
+
# @option options [String] :after add column after this column
|
|
229
|
+
# @option options [Object] :default default value
|
|
230
|
+
# @option options [Boolean] :null whether column is nullable
|
|
231
|
+
# @return [void]
|
|
232
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
233
|
+
def add_column(table_name, column_name, type, **options)
|
|
234
|
+
sql_type = type_to_sql(type, **options)
|
|
235
|
+
|
|
236
|
+
# Handle nullable
|
|
237
|
+
if options[:null] != false && !sql_type.match?(/^Nullable/i)
|
|
238
|
+
sql_type = "Nullable(#{sql_type})"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{sql_type}"
|
|
242
|
+
|
|
243
|
+
# Add AFTER clause if specified
|
|
244
|
+
if options[:after]
|
|
245
|
+
sql += " AFTER #{quote_column_name(options[:after])}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Add DEFAULT if specified
|
|
249
|
+
if options.key?(:default)
|
|
250
|
+
sql += " DEFAULT #{quote(options[:default])}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
execute(sql, 'ADD COLUMN')
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Remove a column from a table
|
|
257
|
+
#
|
|
258
|
+
# @param table_name [String] the table name
|
|
259
|
+
# @param column_name [String] the column name
|
|
260
|
+
# @param options [Hash] options (unused)
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
263
|
+
def remove_column(table_name, column_name, _type = nil, **options)
|
|
264
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
|
|
265
|
+
execute(sql, 'DROP COLUMN')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Rename a column
|
|
269
|
+
#
|
|
270
|
+
# @param table_name [String] the table name
|
|
271
|
+
# @param old_name [String] the current column name
|
|
272
|
+
# @param new_name [String] the new column name
|
|
273
|
+
# @return [void]
|
|
274
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
275
|
+
def rename_column(table_name, old_name, new_name)
|
|
276
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}"
|
|
277
|
+
execute(sql, 'RENAME COLUMN')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Change a column's type
|
|
281
|
+
#
|
|
282
|
+
# @param table_name [String] the table name
|
|
283
|
+
# @param column_name [String] the column name
|
|
284
|
+
# @param type [Symbol, String] the new column type
|
|
285
|
+
# @param options [Hash] column options
|
|
286
|
+
# @return [void]
|
|
287
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
288
|
+
def change_column(table_name, column_name, type, **options)
|
|
289
|
+
sql_type = type_to_sql(type, **options)
|
|
290
|
+
|
|
291
|
+
# Handle nullable
|
|
292
|
+
if options[:null] != false && !sql_type.match?(/^Nullable/i)
|
|
293
|
+
sql_type = "Nullable(#{sql_type})"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY COLUMN #{quote_column_name(column_name)} #{sql_type}"
|
|
297
|
+
|
|
298
|
+
# Add DEFAULT if specified
|
|
299
|
+
if options.key?(:default)
|
|
300
|
+
sql += " DEFAULT #{quote(options[:default])}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
execute(sql, 'MODIFY COLUMN')
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Add an index to a table
|
|
307
|
+
# ClickHouse uses data skipping indexes (minmax, set, bloom_filter, etc.)
|
|
308
|
+
#
|
|
309
|
+
# @param table_name [String] the table name
|
|
310
|
+
# @param column_name [String, Array<String>] the column(s) to index
|
|
311
|
+
# @param options [Hash] index options
|
|
312
|
+
# @option options [String] :name the index name
|
|
313
|
+
# @option options [String] :type the index type (minmax, set, bloom_filter, etc.)
|
|
314
|
+
# @option options [Integer] :granularity the index granularity
|
|
315
|
+
# @return [void]
|
|
316
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
317
|
+
def add_index(table_name, column_name, **options)
|
|
318
|
+
columns = Array(column_name).map { |c| quote_column_name(c) }.join(', ')
|
|
319
|
+
index_name = options[:name] || "idx_#{Array(column_name).join('_')}"
|
|
320
|
+
index_type = options[:type] || 'minmax'
|
|
321
|
+
granularity = options[:granularity] || 1
|
|
322
|
+
|
|
323
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} ADD INDEX #{quote_column_name(index_name)} (#{columns}) TYPE #{index_type} GRANULARITY #{granularity}"
|
|
324
|
+
execute(sql, 'ADD INDEX')
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Remove an index from a table
|
|
328
|
+
#
|
|
329
|
+
# @param table_name [String] the table name
|
|
330
|
+
# @param options_or_column [Hash, String, Symbol] index name or options with :name
|
|
331
|
+
# @return [void]
|
|
332
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
333
|
+
def remove_index(table_name, options_or_column = nil, **options)
|
|
334
|
+
index_name = if options_or_column.is_a?(Hash)
|
|
335
|
+
options_or_column[:name]
|
|
336
|
+
elsif options[:name]
|
|
337
|
+
options[:name]
|
|
338
|
+
else
|
|
339
|
+
"idx_#{Array(options_or_column).join('_')}"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
|
|
343
|
+
execute(sql, 'DROP INDEX')
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Check if an index exists
|
|
347
|
+
#
|
|
348
|
+
# @param table_name [String] the table name
|
|
349
|
+
# @param index_name [String] the index name
|
|
350
|
+
# @return [Boolean] true if the index exists
|
|
351
|
+
def index_exists?(table_name, index_name)
|
|
352
|
+
sql = <<~SQL
|
|
353
|
+
SELECT 1
|
|
354
|
+
FROM system.data_skipping_indices
|
|
355
|
+
WHERE database = currentDatabase()
|
|
356
|
+
AND table = '#{quote_string(table_name.to_s)}'
|
|
357
|
+
AND name = '#{quote_string(index_name.to_s)}'
|
|
358
|
+
LIMIT 1
|
|
359
|
+
SQL
|
|
360
|
+
|
|
361
|
+
result = execute(sql, 'SCHEMA')
|
|
362
|
+
result.any?
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Check if a column exists
|
|
366
|
+
#
|
|
367
|
+
# @param table_name [String] the table name
|
|
368
|
+
# @param column_name [String] the column name
|
|
369
|
+
# @return [Boolean] true if the column exists
|
|
370
|
+
def column_exists?(table_name, column_name, type = nil, **options)
|
|
371
|
+
sql = <<~SQL
|
|
372
|
+
SELECT type
|
|
373
|
+
FROM system.columns
|
|
374
|
+
WHERE database = currentDatabase()
|
|
375
|
+
AND table = '#{quote_string(table_name.to_s)}'
|
|
376
|
+
AND name = '#{quote_string(column_name.to_s)}'
|
|
377
|
+
LIMIT 1
|
|
378
|
+
SQL
|
|
379
|
+
|
|
380
|
+
result = execute(sql, 'SCHEMA')
|
|
381
|
+
return false if result.empty?
|
|
382
|
+
|
|
383
|
+
if type
|
|
384
|
+
# Check if type matches
|
|
385
|
+
column_type = result.first['type']
|
|
386
|
+
expected_type = type_to_sql(type, **options)
|
|
387
|
+
column_type.downcase.include?(expected_type.downcase)
|
|
388
|
+
else
|
|
389
|
+
true
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Get the current database name
|
|
394
|
+
#
|
|
395
|
+
# @return [String] the database name
|
|
396
|
+
def current_database
|
|
397
|
+
result = execute('SELECT currentDatabase() AS db', 'SCHEMA')
|
|
398
|
+
result.first['db']
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# List all databases
|
|
402
|
+
#
|
|
403
|
+
# @return [Array<String>] list of database names
|
|
404
|
+
def databases
|
|
405
|
+
result = execute('SELECT name FROM system.databases ORDER BY name', 'SCHEMA')
|
|
406
|
+
result.map { |row| row['name'] }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Create a database
|
|
410
|
+
#
|
|
411
|
+
# @param database_name [String] the database name
|
|
412
|
+
# @param options [Hash] database options
|
|
413
|
+
# @option options [Boolean] :if_not_exists add IF NOT EXISTS clause
|
|
414
|
+
# @return [void]
|
|
415
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
416
|
+
def create_database(database_name, **options)
|
|
417
|
+
if_not_exists = options.fetch(:if_not_exists, false)
|
|
418
|
+
sql = "CREATE DATABASE #{if_not_exists ? 'IF NOT EXISTS ' : ''}`#{database_name}`"
|
|
419
|
+
execute(sql, 'CREATE DATABASE')
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Drop a database
|
|
423
|
+
#
|
|
424
|
+
# @param database_name [String] the database name
|
|
425
|
+
# @param options [Hash] drop options
|
|
426
|
+
# @option options [Boolean] :if_exists add IF EXISTS clause
|
|
427
|
+
# @return [void]
|
|
428
|
+
# @raise [ClickhouseRuby::QueryError] on error
|
|
429
|
+
def drop_database(database_name, **options)
|
|
430
|
+
if_exists = options.fetch(:if_exists, false)
|
|
431
|
+
sql = "DROP DATABASE #{if_exists ? 'IF EXISTS ' : ''}`#{database_name}`"
|
|
432
|
+
execute(sql, 'DROP DATABASE')
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
private
|
|
436
|
+
|
|
437
|
+
# Convert a Rails type to ClickHouse SQL type
|
|
438
|
+
#
|
|
439
|
+
# @param type [Symbol, String] the Rails type
|
|
440
|
+
# @param options [Hash] type options
|
|
441
|
+
# @return [String] the ClickHouse SQL type
|
|
442
|
+
def type_to_sql(type, **options)
|
|
443
|
+
type = type.to_sym if type.respond_to?(:to_sym)
|
|
444
|
+
|
|
445
|
+
case type
|
|
446
|
+
when :primary_key
|
|
447
|
+
'UInt64'
|
|
448
|
+
when :string, :text
|
|
449
|
+
if options[:limit]
|
|
450
|
+
"FixedString(#{options[:limit]})"
|
|
451
|
+
else
|
|
452
|
+
'String'
|
|
453
|
+
end
|
|
454
|
+
when :integer
|
|
455
|
+
case options[:limit]
|
|
456
|
+
when 1 then 'Int8'
|
|
457
|
+
when 2 then 'Int16'
|
|
458
|
+
when 3, 4 then 'Int32'
|
|
459
|
+
when 5, 6, 7, 8 then 'Int64'
|
|
460
|
+
else 'Int32'
|
|
461
|
+
end
|
|
462
|
+
when :bigint
|
|
463
|
+
'Int64'
|
|
464
|
+
when :float
|
|
465
|
+
options[:limit] == 8 ? 'Float64' : 'Float32'
|
|
466
|
+
when :decimal
|
|
467
|
+
precision = options[:precision] || 10
|
|
468
|
+
scale = options[:scale] || 0
|
|
469
|
+
"Decimal(#{precision}, #{scale})"
|
|
470
|
+
when :datetime
|
|
471
|
+
if options[:precision]
|
|
472
|
+
"DateTime64(#{options[:precision]})"
|
|
473
|
+
else
|
|
474
|
+
'DateTime'
|
|
475
|
+
end
|
|
476
|
+
when :timestamp
|
|
477
|
+
"DateTime64(#{options[:precision] || 3})"
|
|
478
|
+
when :time
|
|
479
|
+
'DateTime'
|
|
480
|
+
when :date
|
|
481
|
+
'Date'
|
|
482
|
+
when :binary
|
|
483
|
+
'String'
|
|
484
|
+
when :boolean
|
|
485
|
+
'UInt8'
|
|
486
|
+
when :uuid
|
|
487
|
+
'UUID'
|
|
488
|
+
when :json
|
|
489
|
+
'String'
|
|
490
|
+
else
|
|
491
|
+
# Return as-is if it's a ClickHouse type
|
|
492
|
+
type.to_s
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Create a new column object
|
|
497
|
+
#
|
|
498
|
+
# @param name [String] column name
|
|
499
|
+
# @param default [Object] default value
|
|
500
|
+
# @param sql_type_metadata [Object] type metadata
|
|
501
|
+
# @param null [Boolean] nullable
|
|
502
|
+
# @param comment [String] column comment
|
|
503
|
+
# @return [ActiveRecord::ConnectionAdapters::Column]
|
|
504
|
+
def new_column(name, default, sql_type_metadata, null, comment = nil)
|
|
505
|
+
::ActiveRecord::ConnectionAdapters::Column.new(
|
|
506
|
+
name,
|
|
507
|
+
default,
|
|
508
|
+
sql_type_metadata,
|
|
509
|
+
null,
|
|
510
|
+
comment: comment
|
|
511
|
+
)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Fetch type metadata for a column type
|
|
515
|
+
#
|
|
516
|
+
# @param sql_type [String] the SQL type string
|
|
517
|
+
# @return [ActiveRecord::ConnectionAdapters::SqlTypeMetadata]
|
|
518
|
+
def fetch_type_metadata(sql_type)
|
|
519
|
+
cast_type = lookup_cast_type(sql_type)
|
|
520
|
+
::ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
|
521
|
+
sql_type: sql_type,
|
|
522
|
+
type: cast_type.type,
|
|
523
|
+
limit: cast_type.limit,
|
|
524
|
+
precision: cast_type.precision,
|
|
525
|
+
scale: cast_type.scale
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Look up the cast type for a SQL type
|
|
530
|
+
#
|
|
531
|
+
# @param sql_type [String] the SQL type string
|
|
532
|
+
# @return [ActiveRecord::Type::Value] the type object
|
|
533
|
+
def lookup_cast_type(sql_type)
|
|
534
|
+
type_map.lookup(sql_type)
|
|
535
|
+
rescue KeyError
|
|
536
|
+
# Fall back to string if type not found
|
|
537
|
+
::ActiveRecord::Type::String.new
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Create a table definition object
|
|
541
|
+
#
|
|
542
|
+
# @param table_name [String] the table name
|
|
543
|
+
# @param options [Hash] table options
|
|
544
|
+
# @return [TableDefinition]
|
|
545
|
+
def create_table_definition(table_name, **options)
|
|
546
|
+
TableDefinition.new(self, table_name, **options)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Get the schema creation object
|
|
550
|
+
#
|
|
551
|
+
# @return [SchemaCreation]
|
|
552
|
+
def schema_creation
|
|
553
|
+
SchemaCreation.new(self)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Table definition for ClickHouse CREATE TABLE statements
|
|
558
|
+
class TableDefinition
|
|
559
|
+
attr_reader :name, :columns, :options
|
|
560
|
+
|
|
561
|
+
def initialize(adapter, name, **options)
|
|
562
|
+
@adapter = adapter
|
|
563
|
+
@name = name
|
|
564
|
+
@columns = []
|
|
565
|
+
@options = options
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Add a column to the table definition
|
|
569
|
+
#
|
|
570
|
+
# @param name [String, Symbol] the column name
|
|
571
|
+
# @param type [Symbol, String] the column type
|
|
572
|
+
# @param options [Hash] column options
|
|
573
|
+
# @return [self]
|
|
574
|
+
def column(name, type, **options)
|
|
575
|
+
@columns << { name: name.to_s, type: type, options: options }
|
|
576
|
+
self
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Shorthand methods for common types
|
|
580
|
+
%i[string text integer bigint float decimal datetime timestamp date binary boolean uuid].each do |type|
|
|
581
|
+
define_method(type) do |name, **options|
|
|
582
|
+
column(name, type, **options)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Primary key column (UInt64 for ClickHouse)
|
|
587
|
+
def primary_key(name, type = :primary_key, **options)
|
|
588
|
+
column(name, type, **options)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Timestamps (created_at, updated_at)
|
|
592
|
+
def timestamps(**options)
|
|
593
|
+
column(:created_at, :datetime, **options)
|
|
594
|
+
column(:updated_at, :datetime, **options)
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Schema creation for ClickHouse DDL statements
|
|
599
|
+
class SchemaCreation
|
|
600
|
+
def initialize(adapter)
|
|
601
|
+
@adapter = adapter
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Generate CREATE TABLE SQL from a TableDefinition
|
|
605
|
+
#
|
|
606
|
+
# @param table_definition [TableDefinition] the table definition
|
|
607
|
+
# @return [String] the CREATE TABLE SQL
|
|
608
|
+
def accept(table_definition)
|
|
609
|
+
columns_sql = table_definition.columns.map do |col|
|
|
610
|
+
column_sql(col)
|
|
611
|
+
end.join(",\n ")
|
|
612
|
+
|
|
613
|
+
engine = table_definition.options[:engine] || 'MergeTree'
|
|
614
|
+
order_by = table_definition.options[:order_by]
|
|
615
|
+
partition_by = table_definition.options[:partition_by]
|
|
616
|
+
primary_key = table_definition.options[:primary_key]
|
|
617
|
+
settings = table_definition.options[:settings]
|
|
618
|
+
|
|
619
|
+
sql = "CREATE TABLE #{@adapter.quote_table_name(table_definition.name)} (\n #{columns_sql}\n)"
|
|
620
|
+
sql += "\nENGINE = #{engine}"
|
|
621
|
+
sql += "\nORDER BY (#{order_by})" if order_by
|
|
622
|
+
sql += "\nPARTITION BY #{partition_by}" if partition_by
|
|
623
|
+
sql += "\nPRIMARY KEY (#{primary_key})" if primary_key
|
|
624
|
+
sql += "\nSETTINGS #{settings}" if settings
|
|
625
|
+
|
|
626
|
+
sql
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
private
|
|
630
|
+
|
|
631
|
+
def column_sql(col)
|
|
632
|
+
type = type_to_sql(col[:type], **col[:options])
|
|
633
|
+
|
|
634
|
+
# Handle nullable
|
|
635
|
+
if col[:options][:null] != false && !type.match?(/^Nullable/i) && !col[:type].to_s.match?(/primary_key/)
|
|
636
|
+
type = "Nullable(#{type})"
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
sql = "#{@adapter.quote_column_name(col[:name])} #{type}"
|
|
640
|
+
|
|
641
|
+
# Add DEFAULT if specified
|
|
642
|
+
if col[:options].key?(:default)
|
|
643
|
+
sql += " DEFAULT #{@adapter.quote(col[:options][:default])}"
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
sql
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def type_to_sql(type, **options)
|
|
650
|
+
type = type.to_sym if type.respond_to?(:to_sym)
|
|
651
|
+
|
|
652
|
+
case type
|
|
653
|
+
when :primary_key
|
|
654
|
+
'UInt64'
|
|
655
|
+
when :string, :text
|
|
656
|
+
options[:limit] ? "FixedString(#{options[:limit]})" : 'String'
|
|
657
|
+
when :integer
|
|
658
|
+
case options[:limit]
|
|
659
|
+
when 1 then 'Int8'
|
|
660
|
+
when 2 then 'Int16'
|
|
661
|
+
when 3, 4 then 'Int32'
|
|
662
|
+
when 5, 6, 7, 8 then 'Int64'
|
|
663
|
+
else 'Int32'
|
|
664
|
+
end
|
|
665
|
+
when :bigint
|
|
666
|
+
'Int64'
|
|
667
|
+
when :float
|
|
668
|
+
options[:limit] == 8 ? 'Float64' : 'Float32'
|
|
669
|
+
when :decimal
|
|
670
|
+
"Decimal(#{options[:precision] || 10}, #{options[:scale] || 0})"
|
|
671
|
+
when :datetime
|
|
672
|
+
options[:precision] ? "DateTime64(#{options[:precision]})" : 'DateTime'
|
|
673
|
+
when :timestamp
|
|
674
|
+
"DateTime64(#{options[:precision] || 3})"
|
|
675
|
+
when :time
|
|
676
|
+
'DateTime'
|
|
677
|
+
when :date
|
|
678
|
+
'Date'
|
|
679
|
+
when :binary
|
|
680
|
+
'String'
|
|
681
|
+
when :boolean
|
|
682
|
+
'UInt8'
|
|
683
|
+
when :uuid
|
|
684
|
+
'UUID'
|
|
685
|
+
when :json
|
|
686
|
+
'String'
|
|
687
|
+
else
|
|
688
|
+
type.to_s
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
end
|