clickhouse-ruby 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +142 -1
- data/lib/clickhouse_ruby/active_record/generators/migration_generator.rb +308 -0
- data/lib/clickhouse_ruby/active_record/generators/templates/create_table.rb.tt +59 -0
- data/lib/clickhouse_ruby/active_record/generators/templates/migration.rb.tt +87 -0
- data/lib/clickhouse_ruby/active_record/railtie.rb +16 -2
- data/lib/clickhouse_ruby/active_record/schema_dumper.rb +458 -0
- data/lib/clickhouse_ruby/active_record.rb +1 -0
- data/lib/clickhouse_ruby/client.rb +212 -1
- data/lib/clickhouse_ruby/connection_pool.rb +73 -1
- data/lib/clickhouse_ruby/instrumentation.rb +176 -0
- data/lib/clickhouse_ruby/version.rb +1 -1
- data/lib/clickhouse_ruby.rb +1 -0
- metadata +20 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
<%- if @migration_action == :create_table -%>
|
|
5
|
+
def change
|
|
6
|
+
create_table :<%= table_name %>, **clickhouse_options do |t|
|
|
7
|
+
<%- attributes.each do |attribute| -%>
|
|
8
|
+
<%- if attribute.type == :references -%>
|
|
9
|
+
t.bigint :<%= attribute.name %>_id<%= attribute.has_index? ? ", index: true" : "" %>
|
|
10
|
+
<%- else -%>
|
|
11
|
+
t.<%= attribute.type %> :<%= attribute.name %>
|
|
12
|
+
<%- end -%>
|
|
13
|
+
<%- end -%>
|
|
14
|
+
<%- if attributes.empty? -%>
|
|
15
|
+
t.uuid :id
|
|
16
|
+
<%- end -%>
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# ClickHouse-specific table options
|
|
24
|
+
#
|
|
25
|
+
# Customize these options based on your data patterns:
|
|
26
|
+
# - engine: Table engine (MergeTree family for analytics)
|
|
27
|
+
# - order_by: Determines data sorting and query performance
|
|
28
|
+
# - partition_by: Data partitioning for faster queries and easier maintenance
|
|
29
|
+
# - primary_key: Sparse index for faster lookups (defaults to order_by)
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash] ClickHouse table options
|
|
32
|
+
def clickhouse_options
|
|
33
|
+
{
|
|
34
|
+
engine: "<%= engine_with_cluster %>",
|
|
35
|
+
<%- if order_by_clause -%>
|
|
36
|
+
order_by: "<%= order_by_clause %>",
|
|
37
|
+
<%- end -%>
|
|
38
|
+
<%- if partition_by_clause -%>
|
|
39
|
+
partition_by: "<%= partition_by_clause %>",
|
|
40
|
+
<%- end -%>
|
|
41
|
+
<%- if primary_key_clause -%>
|
|
42
|
+
primary_key: "<%= primary_key_clause %>",
|
|
43
|
+
<%- end -%>
|
|
44
|
+
<%- if settings_clause -%>
|
|
45
|
+
settings: "<%= settings_clause %>",
|
|
46
|
+
<%- end -%>
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
<%- elsif @migration_action == :add_column -%>
|
|
50
|
+
def change
|
|
51
|
+
<%- attributes.each do |attribute| -%>
|
|
52
|
+
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %>
|
|
53
|
+
<%- end -%>
|
|
54
|
+
<%- if attributes.empty? && column_name -%>
|
|
55
|
+
add_column :<%= table_name %>, :<%= column_name %>, :string
|
|
56
|
+
<%- end -%>
|
|
57
|
+
end
|
|
58
|
+
<%- elsif @migration_action == :remove_column -%>
|
|
59
|
+
def change
|
|
60
|
+
<%- attributes.each do |attribute| -%>
|
|
61
|
+
remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %>
|
|
62
|
+
<%- end -%>
|
|
63
|
+
<%- if attributes.empty? && column_name -%>
|
|
64
|
+
# Note: Specify the column type for reversible migration
|
|
65
|
+
remove_column :<%= table_name %>, :<%= column_name %>, :string
|
|
66
|
+
<%- end -%>
|
|
67
|
+
end
|
|
68
|
+
<%- else -%>
|
|
69
|
+
def up
|
|
70
|
+
# Add your ClickHouse migration code here
|
|
71
|
+
# Example:
|
|
72
|
+
# execute <<~SQL
|
|
73
|
+
# ALTER TABLE <%= table_name %>
|
|
74
|
+
# ADD COLUMN new_column String
|
|
75
|
+
# SQL
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def down
|
|
79
|
+
# Reverse the migration
|
|
80
|
+
# Example:
|
|
81
|
+
# execute <<~SQL
|
|
82
|
+
# ALTER TABLE <%= table_name %>
|
|
83
|
+
# DROP COLUMN new_column
|
|
84
|
+
# SQL
|
|
85
|
+
end
|
|
86
|
+
<%- end -%>
|
|
87
|
+
end
|
|
@@ -8,6 +8,8 @@ module ClickhouseRuby
|
|
|
8
8
|
#
|
|
9
9
|
# This Railtie hooks into Rails to:
|
|
10
10
|
# - Register the ClickHouse adapter with ActiveRecord
|
|
11
|
+
# - Register the ClickHouse migration generator
|
|
12
|
+
# - Register the ClickHouse schema dumper
|
|
11
13
|
# - Configure default settings for Rails environments
|
|
12
14
|
# - Set up logging integration
|
|
13
15
|
#
|
|
@@ -26,6 +28,10 @@ module ClickhouseRuby
|
|
|
26
28
|
# ssl: true
|
|
27
29
|
# ssl_verify: true
|
|
28
30
|
#
|
|
31
|
+
# @example Generate a ClickHouse migration
|
|
32
|
+
# rails generate clickhouse:migration CreateEvents user_id:integer name:string
|
|
33
|
+
# rails generate clickhouse:migration CreateEvents --engine=ReplacingMergeTree --order-by=user_id
|
|
34
|
+
#
|
|
29
35
|
class Railtie < ::Rails::Railtie
|
|
30
36
|
# Initialize the adapter when ActiveRecord loads
|
|
31
37
|
initializer "clickhouse_ruby.initialize_active_record" do
|
|
@@ -51,6 +57,13 @@ module ClickhouseRuby
|
|
|
51
57
|
end
|
|
52
58
|
end
|
|
53
59
|
|
|
60
|
+
# Load the schema dumper when ActiveRecord loads
|
|
61
|
+
initializer "clickhouse_ruby.load_schema_dumper" do
|
|
62
|
+
::ActiveSupport.on_load(:active_record) do
|
|
63
|
+
require_relative "schema_dumper"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
54
67
|
# Configure the connection pool for Rails
|
|
55
68
|
config.after_initialize do
|
|
56
69
|
# Set up connection pool based on Rails configuration
|
|
@@ -66,7 +79,7 @@ module ClickhouseRuby
|
|
|
66
79
|
|
|
67
80
|
# Add generators namespace for Rails generators
|
|
68
81
|
generators do
|
|
69
|
-
require_relative "generators/migration_generator"
|
|
82
|
+
require_relative "generators/migration_generator"
|
|
70
83
|
end
|
|
71
84
|
|
|
72
85
|
# Log deprecation warnings for known issues
|
|
@@ -74,7 +87,8 @@ module ClickhouseRuby
|
|
|
74
87
|
::ActiveSupport.on_load(:active_record) do
|
|
75
88
|
# Warn about features that don't work with ClickHouse
|
|
76
89
|
if defined?(Rails.logger) && Rails.logger
|
|
77
|
-
Rails.logger.debug "[ClickhouseRuby] Note: ClickHouse does not support
|
|
90
|
+
Rails.logger.debug "[ClickhouseRuby] Note: ClickHouse does not support " \
|
|
91
|
+
"transactions, savepoints, or foreign keys"
|
|
78
92
|
end
|
|
79
93
|
end
|
|
80
94
|
end
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/schema_dumper"
|
|
4
|
+
|
|
5
|
+
module ClickhouseRuby
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
# Custom schema dumper for ClickHouse databases
|
|
8
|
+
#
|
|
9
|
+
# Extends ActiveRecord::SchemaDumper to properly dump ClickHouse-specific
|
|
10
|
+
# schema elements like engines, ORDER BY, PARTITION BY, and SETTINGS.
|
|
11
|
+
#
|
|
12
|
+
# @example Usage
|
|
13
|
+
# # Automatically used when running:
|
|
14
|
+
# rails db:schema:dump
|
|
15
|
+
#
|
|
16
|
+
# @example Manual usage
|
|
17
|
+
# File.open("db/schema.rb", "w") do |file|
|
|
18
|
+
# ClickhouseRuby::ActiveRecord::SchemaDumper.dump(connection, file)
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
class SchemaDumper < ::ActiveRecord::SchemaDumper
|
|
22
|
+
# Dump the schema to a stream
|
|
23
|
+
#
|
|
24
|
+
# @param connection [ConnectionAdapter] the database connection
|
|
25
|
+
# @param stream [IO] the output stream
|
|
26
|
+
# @param _config [ActiveRecord::DatabaseConfigurations::DatabaseConfig] database config (unused)
|
|
27
|
+
# @return [void]
|
|
28
|
+
def self.dump(connection = ::ActiveRecord::Base.connection, stream = $stdout, _config = nil)
|
|
29
|
+
new(connection, generate_options).dump(stream)
|
|
30
|
+
stream
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate options for the dumper
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] dumper options
|
|
36
|
+
def self.generate_options
|
|
37
|
+
{
|
|
38
|
+
table_name_prefix: ::ActiveRecord::Base.table_name_prefix,
|
|
39
|
+
table_name_suffix: ::ActiveRecord::Base.table_name_suffix,
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Dump all tables to the stream
|
|
46
|
+
#
|
|
47
|
+
# @param stream [IO] the output stream
|
|
48
|
+
# @return [void]
|
|
49
|
+
def tables(stream)
|
|
50
|
+
table_names = @connection.tables.sort
|
|
51
|
+
|
|
52
|
+
table_names.each do |table_name|
|
|
53
|
+
table(table_name, stream)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Dump views after tables
|
|
57
|
+
views(stream) if @connection.respond_to?(:views)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Dump views to the stream
|
|
61
|
+
#
|
|
62
|
+
# @param stream [IO] the output stream
|
|
63
|
+
# @return [void]
|
|
64
|
+
def views(stream)
|
|
65
|
+
return unless @connection.respond_to?(:views)
|
|
66
|
+
|
|
67
|
+
view_names = @connection.views.sort
|
|
68
|
+
return if view_names.empty?
|
|
69
|
+
|
|
70
|
+
stream.puts
|
|
71
|
+
stream.puts " # Views"
|
|
72
|
+
|
|
73
|
+
view_names.each do |view_name|
|
|
74
|
+
view(view_name, stream)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Dump a single table to the stream
|
|
79
|
+
#
|
|
80
|
+
# @param table_name [String] the table name
|
|
81
|
+
# @param stream [IO] the output stream
|
|
82
|
+
# @return [void]
|
|
83
|
+
def table(table_name, stream)
|
|
84
|
+
columns = @connection.columns(table_name)
|
|
85
|
+
table_options = TableOptionsExtractor.new(@connection, table_name).extract
|
|
86
|
+
|
|
87
|
+
# Begin create_table block
|
|
88
|
+
stream.print " create_table #{table_name.inspect}"
|
|
89
|
+
stream.print ", #{format_options(table_options)}" unless table_options.empty?
|
|
90
|
+
stream.puts " do |t|"
|
|
91
|
+
|
|
92
|
+
# Dump columns
|
|
93
|
+
columns.each do |column|
|
|
94
|
+
ColumnDumper.new(column, stream).dump
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
stream.puts " end"
|
|
98
|
+
stream.puts
|
|
99
|
+
|
|
100
|
+
# Dump indexes
|
|
101
|
+
dump_indexes(table_name, stream)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Dump a view definition to the stream
|
|
105
|
+
#
|
|
106
|
+
# @param view_name [String] the view name
|
|
107
|
+
# @param stream [IO] the output stream
|
|
108
|
+
# @return [void]
|
|
109
|
+
def view(view_name, stream)
|
|
110
|
+
view_definition = extract_view_definition(view_name)
|
|
111
|
+
return unless view_definition
|
|
112
|
+
|
|
113
|
+
stream.puts " execute <<~SQL"
|
|
114
|
+
stream.puts " #{view_definition}"
|
|
115
|
+
stream.puts " SQL"
|
|
116
|
+
stream.puts
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extract view definition
|
|
120
|
+
#
|
|
121
|
+
# @param view_name [String] the view name
|
|
122
|
+
# @return [String, nil] the CREATE VIEW statement or nil
|
|
123
|
+
def extract_view_definition(view_name)
|
|
124
|
+
sql = "SHOW CREATE TABLE `#{@connection.quote_string(view_name)}`"
|
|
125
|
+
result = @connection.execute(sql, "SCHEMA")
|
|
126
|
+
return nil if result.empty?
|
|
127
|
+
|
|
128
|
+
result.first["statement"] || result.first["Create Table"]
|
|
129
|
+
rescue StandardError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format options hash as Ruby code
|
|
134
|
+
#
|
|
135
|
+
# @param options [Hash] options hash
|
|
136
|
+
# @return [String] formatted options string
|
|
137
|
+
def format_options(options)
|
|
138
|
+
options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Dump indexes for a table
|
|
142
|
+
#
|
|
143
|
+
# @param table_name [String] the table name
|
|
144
|
+
# @param stream [IO] the output stream
|
|
145
|
+
# @return [void]
|
|
146
|
+
def dump_indexes(table_name, stream)
|
|
147
|
+
return unless @connection.respond_to?(:indexes)
|
|
148
|
+
|
|
149
|
+
table_indexes = @connection.indexes(table_name)
|
|
150
|
+
return if table_indexes.empty?
|
|
151
|
+
|
|
152
|
+
table_indexes.each do |index|
|
|
153
|
+
dump_single_index(table_name, index, stream)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
stream.puts
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Dump a single index
|
|
160
|
+
#
|
|
161
|
+
# @param table_name [String] the table name
|
|
162
|
+
# @param index [Hash] the index information
|
|
163
|
+
# @param stream [IO] the output stream
|
|
164
|
+
# @return [void]
|
|
165
|
+
def dump_single_index(table_name, index, stream)
|
|
166
|
+
stream.print " add_index #{table_name.inspect}"
|
|
167
|
+
stream.print ", #{index[:expression].inspect}"
|
|
168
|
+
stream.print ", name: #{index[:name].inspect}"
|
|
169
|
+
stream.print ", type: #{index[:type].inspect}" if index[:type]
|
|
170
|
+
stream.print ", granularity: #{index[:granularity]}" if index[:granularity]
|
|
171
|
+
stream.puts
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Header comment for the schema file
|
|
175
|
+
#
|
|
176
|
+
# @param stream [IO] the output stream
|
|
177
|
+
# @return [void]
|
|
178
|
+
def header(stream)
|
|
179
|
+
write_header_comments(stream)
|
|
180
|
+
stream.puts
|
|
181
|
+
stream.puts "ActiveRecord::Schema[#{::ActiveRecord::Migration.current_version}].define(" \
|
|
182
|
+
"version: #{schema_version}) do"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Write header comments to stream
|
|
186
|
+
#
|
|
187
|
+
# @param stream [IO] the output stream
|
|
188
|
+
# @return [void]
|
|
189
|
+
def write_header_comments(stream)
|
|
190
|
+
stream.puts "# This file is auto-generated from the current state of the database. Instead"
|
|
191
|
+
stream.puts "# of editing this file, please use the migrations feature of Active Record to"
|
|
192
|
+
stream.puts "# incrementally modify your database, and then regenerate this schema definition."
|
|
193
|
+
stream.puts "#"
|
|
194
|
+
stream.puts "# This file is the source Rails uses to define your schema when running"
|
|
195
|
+
stream.puts "# `bin/rails db:schema:load`."
|
|
196
|
+
stream.puts "#"
|
|
197
|
+
stream.puts "# Note: ClickHouse-specific options (engine, order_by, partition_by) are preserved"
|
|
198
|
+
stream.puts "# and required for proper table recreation."
|
|
199
|
+
stream.puts "#"
|
|
200
|
+
stream.puts "# Database: ClickHouse"
|
|
201
|
+
stream.puts "# Adapter: clickhouse_ruby"
|
|
202
|
+
stream.puts "#"
|
|
203
|
+
stream.puts "# It's strongly recommended that you check this file into version control."
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Get the current schema version
|
|
207
|
+
#
|
|
208
|
+
# @return [String] the schema version
|
|
209
|
+
def schema_version
|
|
210
|
+
if @connection.respond_to?(:migration_context)
|
|
211
|
+
@connection.migration_context.current_version.to_s
|
|
212
|
+
else
|
|
213
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Footer for the schema file
|
|
218
|
+
#
|
|
219
|
+
# @param stream [IO] the output stream
|
|
220
|
+
# @return [void]
|
|
221
|
+
def trailer(stream)
|
|
222
|
+
stream.puts "end"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Extracts ClickHouse-specific table options
|
|
227
|
+
class TableOptionsExtractor
|
|
228
|
+
def initialize(connection, table_name)
|
|
229
|
+
@connection = connection
|
|
230
|
+
@table_name = table_name
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Extract table options from system.tables
|
|
234
|
+
#
|
|
235
|
+
# @return [Hash] table options
|
|
236
|
+
def extract
|
|
237
|
+
row = fetch_table_metadata
|
|
238
|
+
return {} unless row
|
|
239
|
+
|
|
240
|
+
build_options(row)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
# Fetch table metadata from system.tables
|
|
246
|
+
#
|
|
247
|
+
# @return [Hash, nil] the table metadata row
|
|
248
|
+
def fetch_table_metadata
|
|
249
|
+
sql = <<~SQL
|
|
250
|
+
SELECT engine, sorting_key, partition_key, primary_key, engine_full
|
|
251
|
+
FROM system.tables
|
|
252
|
+
WHERE database = currentDatabase()
|
|
253
|
+
AND name = '#{@connection.quote_string(@table_name)}'
|
|
254
|
+
SQL
|
|
255
|
+
|
|
256
|
+
result = @connection.execute(sql, "SCHEMA")
|
|
257
|
+
result.first
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Build options hash from metadata row
|
|
261
|
+
#
|
|
262
|
+
# @param row [Hash] the metadata row
|
|
263
|
+
# @return [Hash] the options hash
|
|
264
|
+
def build_options(row)
|
|
265
|
+
options = {}
|
|
266
|
+
options[:engine] = row["engine"] if row["engine"] && row["engine"] != "MergeTree"
|
|
267
|
+
options[:order_by] = row["sorting_key"] if row["sorting_key"].present?
|
|
268
|
+
options[:partition_by] = row["partition_key"] if row["partition_key"].present?
|
|
269
|
+
add_primary_key(options, row)
|
|
270
|
+
add_settings(options, row)
|
|
271
|
+
options
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Add primary key if different from sorting key
|
|
275
|
+
def add_primary_key(options, row)
|
|
276
|
+
return unless row["primary_key"].present? && row["primary_key"] != row["sorting_key"]
|
|
277
|
+
|
|
278
|
+
options[:primary_key] = row["primary_key"]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Add settings from engine_full
|
|
282
|
+
def add_settings(options, row)
|
|
283
|
+
return unless row["engine_full"]&.include?("SETTINGS")
|
|
284
|
+
|
|
285
|
+
settings = row["engine_full"][/SETTINGS\s+(.+)$/i, 1]
|
|
286
|
+
options[:settings] = settings if settings.present?
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Dumps a single column definition
|
|
291
|
+
class ColumnDumper
|
|
292
|
+
def initialize(column, stream)
|
|
293
|
+
@column = column
|
|
294
|
+
@stream = stream
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Dump the column definition
|
|
298
|
+
#
|
|
299
|
+
# @return [void]
|
|
300
|
+
def dump
|
|
301
|
+
type = schema_type
|
|
302
|
+
options = column_options
|
|
303
|
+
|
|
304
|
+
@stream.print " t.#{type} #{@column.name.inspect}"
|
|
305
|
+
@stream.print ", #{format_options(options)}" unless options.empty?
|
|
306
|
+
@stream.puts
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
# Get the schema type for the column
|
|
312
|
+
#
|
|
313
|
+
# @return [Symbol] the schema type
|
|
314
|
+
def schema_type
|
|
315
|
+
SchemaTypeMapper.map(@column.sql_type.to_s)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Extract column options
|
|
319
|
+
#
|
|
320
|
+
# @return [Hash] column options
|
|
321
|
+
def column_options
|
|
322
|
+
ColumnOptionsExtractor.new(@column).extract
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Format options hash as Ruby code
|
|
326
|
+
def format_options(options)
|
|
327
|
+
options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Maps SQL types to schema types
|
|
332
|
+
module SchemaTypeMapper
|
|
333
|
+
# Type patterns and their schema types
|
|
334
|
+
PATTERNS = [
|
|
335
|
+
[/^UInt64$/i, :bigint],
|
|
336
|
+
[/^UInt(8|16|32)$/i, :integer],
|
|
337
|
+
[/^Int(8|16|32)$/i, :integer],
|
|
338
|
+
[/^Int64$/i, :bigint],
|
|
339
|
+
[/^Float(32|64)$/i, :float],
|
|
340
|
+
[/^Decimal/i, :decimal],
|
|
341
|
+
[/^String$/i, :string],
|
|
342
|
+
[/^FixedString/i, :string],
|
|
343
|
+
[/^Date$/i, :date],
|
|
344
|
+
[/^DateTime64/i, :datetime],
|
|
345
|
+
[/^DateTime$/i, :datetime],
|
|
346
|
+
[/^UUID$/i, :uuid],
|
|
347
|
+
].freeze
|
|
348
|
+
|
|
349
|
+
class << self
|
|
350
|
+
# Map a SQL type to schema type
|
|
351
|
+
#
|
|
352
|
+
# @param sql_type [String] the SQL type
|
|
353
|
+
# @return [Symbol] the schema type
|
|
354
|
+
def map(sql_type)
|
|
355
|
+
# Handle wrapper types
|
|
356
|
+
return handle_nullable(sql_type) if sql_type.match?(/^Nullable\(/i)
|
|
357
|
+
return handle_low_cardinality(sql_type) if sql_type.match?(/^LowCardinality\(/i)
|
|
358
|
+
|
|
359
|
+
# Handle standard types
|
|
360
|
+
PATTERNS.each do |pattern, type|
|
|
361
|
+
return type if sql_type.match?(pattern)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
:string
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
private
|
|
368
|
+
|
|
369
|
+
# Handle Nullable wrapper
|
|
370
|
+
def handle_nullable(sql_type)
|
|
371
|
+
inner = sql_type.match(/^Nullable\((.+)\)/i)[1]
|
|
372
|
+
map(inner)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Handle LowCardinality wrapper
|
|
376
|
+
def handle_low_cardinality(sql_type)
|
|
377
|
+
inner = sql_type.match(/^LowCardinality\((.+)\)/i)[1]
|
|
378
|
+
map(inner)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Extracts column options from a column
|
|
384
|
+
class ColumnOptionsExtractor
|
|
385
|
+
def initialize(column)
|
|
386
|
+
@column = column
|
|
387
|
+
@sql_type = column.sql_type.to_s
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Extract all column options
|
|
391
|
+
#
|
|
392
|
+
# @return [Hash] the column options
|
|
393
|
+
def extract
|
|
394
|
+
options = {}
|
|
395
|
+
add_nullable(options)
|
|
396
|
+
add_limit(options)
|
|
397
|
+
add_decimal_options(options)
|
|
398
|
+
add_datetime_precision(options)
|
|
399
|
+
add_default(options)
|
|
400
|
+
add_comment(options)
|
|
401
|
+
options
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
def add_nullable(options)
|
|
407
|
+
options[:null] = true if @sql_type.match?(/^Nullable/i)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def add_limit(options)
|
|
411
|
+
return unless (match = @sql_type.match(/^FixedString\((\d+)\)/i))
|
|
412
|
+
|
|
413
|
+
options[:limit] = match[1].to_i
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def add_decimal_options(options)
|
|
417
|
+
return unless (match = @sql_type.match(/^Decimal\((\d+),\s*(\d+)\)/i))
|
|
418
|
+
|
|
419
|
+
options[:precision] = match[1].to_i
|
|
420
|
+
options[:scale] = match[2].to_i
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def add_datetime_precision(options)
|
|
424
|
+
return unless (match = @sql_type.match(/^DateTime64\((\d+)\)/i))
|
|
425
|
+
|
|
426
|
+
options[:precision] = match[1].to_i
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def add_default(options)
|
|
430
|
+
options[:default] = @column.default if @column.default.present?
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def add_comment(options)
|
|
434
|
+
options[:comment] = @column.comment if @column.comment.present?
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Register the custom schema dumper with ActiveRecord
|
|
441
|
+
if defined?(ActiveRecord::SchemaDumper)
|
|
442
|
+
# Override the default dumper for ClickHouse connections
|
|
443
|
+
module ClickhouseRuby
|
|
444
|
+
module ActiveRecord
|
|
445
|
+
module SchemaDumperExtension
|
|
446
|
+
def dump(connection = ::ActiveRecord::Base.connection, stream = $stdout, config = nil)
|
|
447
|
+
if connection.adapter_name == "Clickhouse"
|
|
448
|
+
ClickhouseRuby::ActiveRecord::SchemaDumper.dump(connection, stream, config)
|
|
449
|
+
else
|
|
450
|
+
super
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
ActiveRecord::SchemaDumper.singleton_class.prepend(ClickhouseRuby::ActiveRecord::SchemaDumperExtension)
|
|
458
|
+
end
|
|
@@ -7,6 +7,7 @@ require_relative "active_record/arel_visitor"
|
|
|
7
7
|
require_relative "active_record/schema_statements"
|
|
8
8
|
require_relative "active_record/relation_extensions"
|
|
9
9
|
require_relative "active_record/connection_adapter"
|
|
10
|
+
require_relative "active_record/schema_dumper"
|
|
10
11
|
|
|
11
12
|
# Load Railtie if Rails is available
|
|
12
13
|
require_relative "active_record/railtie" if defined?(Rails::Railtie)
|