clickhouse-activerecord 0.3.13 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67ea4a63bb140adb5f6de102c38c097ea18f61499a882574952a9982b8d340cf
4
- data.tar.gz: a1d2a759dae9062a20cc0a53bbd1cb64142062ce9ef84047f7e953b7d957e9b2
3
+ metadata.gz: deeb0e0ddf3a6bef04338637ddf814a826f93b4e69779905f28c280a5ca1a400
4
+ data.tar.gz: 80cba4b321f42cb978be4d6bf4740b03274a7bd0ff882b35d4ac0760736fe9a8
5
5
  SHA512:
6
- metadata.gz: 243b4d089d6349d8c12c529fbb80078ffbaa03f0033c3c69be77751846b031398895a1efdd08460699636277695a3e03eb228a398b5887a9426f4f3b63496c31
7
- data.tar.gz: f790bca5af79492390de98ba2dce363e16207d987093c2e94e9f9ab55b9f769a41bae1c4a12d0b9d3757ee91f92604c1df4db17479dbbc85ced0f48294b297f3
6
+ metadata.gz: 8c18476f8984ab7e7c834b712eff4f9d8e3995287b3db6ace0e4e819ac19dc6bd31b149fee7da970223807170b1902ce8d2af81ad8ac8a56bc73641e26111d22
7
+ data.tar.gz: '094cdb750234da2b59efd755c7fef3e4f4bf3484b124d7d2b74fe999ee2a25cddb435324fd88a49248943cb372459df31a3d151905e93118b456231ae94cbd2c'
@@ -1,3 +1,10 @@
1
+ ### Version 0.4.4 (Sep 23, 2020)
2
+
3
+ * Full support migration and rollback database
4
+ * Support cluster and replica. Auto inject to SQL queries.
5
+ * Fix schema dump/load
6
+ * Can dump schema for using PostgreSQL
7
+
1
8
  ### Version 0.3.10 (Dec 20, 2019)
2
9
 
3
10
  * Support structure dump/load [@StoneGod](https://github.com/StoneGod)
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Clickhouse::Activerecord
2
2
 
3
3
  A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
4
- Tested on ClickHouse version 18.14.
4
+ Support ClickHouse version from 19.14 LTS.
5
5
 
6
6
  ## Installation
7
7
 
@@ -31,8 +31,8 @@ default: &default
31
31
  ssl: true # optional for using ssl connection
32
32
  debug: true # use for showing in to log technical information
33
33
  migrations_paths: db/clickhouse # optional, default: db/migrate_clickhouse
34
- cluster: 'cluster_name' # optional for creating tables in cluster
35
- replica: '{shard}' # optional for creating system tables for shards
34
+ cluster_name: 'cluster_name' # optional for creating tables in cluster
35
+ replica_name: '{replica}' # replica macros name, optional for creating replicated tables
36
36
  ```
37
37
 
38
38
  ## Usage in Rails 5
@@ -111,8 +111,7 @@ Migration:
111
111
 
112
112
  $ rails g clickhouse_migration MIGRATION_NAME COLUMNS
113
113
  $ rake clickhouse:migrate
114
-
115
- Rollback migration not supported!
114
+ $ rake clickhouse:rollback
116
115
 
117
116
  ### Dump / Load for multiple using databases
118
117
 
@@ -126,7 +125,11 @@ Schema load from `db/clickhouse_schema.rb` file:
126
125
 
127
126
  $ rake clickhouse:schema:load
128
127
 
129
- We use schema for emulate development or tests environment on PostgreSQL adapter.
128
+ For export schema to PostgreSQL, you need use:
129
+
130
+ $ rake clickhouse:schema:dump -- --simple
131
+
132
+ Schema will be dump to `db/clickhouse_schema_simple.rb`. If default file exists, it will be auto update after migration.
130
133
 
131
134
  Structure dump to `db/clickhouse_structure.sql` file:
132
135
 
@@ -159,6 +162,19 @@ ActionView.maximum(:date)
159
162
  #=> 'Wed, 29 Nov 2017'
160
163
  ```
161
164
 
165
+ ### Using replica and cluster params in connection parameters
166
+
167
+ ```yml
168
+ default: &default
169
+ ***
170
+ cluster_name: 'cluster_name'
171
+ replica_name: '{replica}'
172
+ ```
173
+
174
+ `ON CLUSTER cluster_name` will be attach to all queries create / drop.
175
+
176
+ Engines `MergeTree` and all support replication engines will be replaced to `Replicated***('/clickhouse/tables/cluster_name/database.table', '{replica}')`
177
+
162
178
  ## Donations
163
179
 
164
180
  Donations to this project are going directly to [PNixx](https://github.com/PNixx), the original author of this project:
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_runtime_dependency 'activerecord', '>= 5.2'
28
28
 
29
29
  spec.add_development_dependency 'bundler', '~> 1.15'
30
- spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rake', '~> 13.0'
31
31
  spec.add_development_dependency 'rspec', '~> 3.4'
32
32
  spec.add_development_dependency 'pry', '~> 0.12'
33
33
  end
@@ -6,15 +6,17 @@ module ActiveRecord
6
6
  class SchemaCreation < AbstractAdapter::SchemaCreation# :nodoc:
7
7
 
8
8
  def visit_AddColumnDefinition(o)
9
- +"ADD COLUMN #{accept(o.column)}"
9
+ sql = +"ADD COLUMN #{accept(o.column)}"
10
+ sql << " AFTER " + quote_column_name(o.column.options[:after]) if o.column.options.key?(:after)
11
+ sql
10
12
  end
11
13
 
12
14
  def add_column_options!(sql, options)
13
- sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
14
15
  if options[:null] || options[:null].nil?
15
16
  sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
16
17
  end
17
18
  sql.gsub!(/(\sString)\(\d+\)/, '\1')
19
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
18
20
  sql
19
21
  end
20
22
 
@@ -27,6 +29,45 @@ module ActiveRecord
27
29
 
28
30
  create_sql
29
31
  end
32
+
33
+ def visit_TableDefinition(o)
34
+ create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} "
35
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
36
+ create_sql << "#{quote_table_name(o.name)} "
37
+
38
+ statements = o.columns.map { |c| accept c }
39
+ statements << accept(o.primary_keys) if o.primary_keys
40
+
41
+ create_sql << "(#{statements.join(', ')})" if statements.present?
42
+ add_table_options!(create_sql, table_options(o))
43
+ create_sql << " AS #{to_sql(o.as)}" if o.as
44
+ create_sql
45
+ end
46
+
47
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
48
+ def table_modifier_in_create(o)
49
+ " TEMPORARY" if o.temporary
50
+ " MATERIALIZED" if o.materialized
51
+ end
52
+
53
+ def visit_ChangeColumnDefinition(o)
54
+ column = o.column
55
+ column.sql_type = type_to_sql(column.type, column.options)
56
+ options = column_options(column)
57
+
58
+ quoted_column_name = quote_column_name(o.name)
59
+ type = column.sql_type
60
+ type = "Nullable(#{type})" if options[:null]
61
+ change_column_sql = +"MODIFY COLUMN #{quoted_column_name} #{type}"
62
+
63
+ if options.key?(:default)
64
+ quoted_default = quote_default_expression(options[:default], column)
65
+ change_column_sql << " DEFAULT #{quoted_default}"
66
+ end
67
+
68
+ change_column_sql
69
+ end
70
+
30
71
  end
31
72
  end
32
73
  end
@@ -5,6 +5,35 @@ module ActiveRecord
5
5
  module Clickhouse
6
6
  class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
7
7
 
8
+ attr_reader :view, :materialized, :if_not_exists
9
+
10
+ def initialize(
11
+ conn,
12
+ name,
13
+ temporary: false,
14
+ if_not_exists: false,
15
+ options: nil,
16
+ as: nil,
17
+ comment: nil,
18
+ view: false,
19
+ materialized: false,
20
+ **
21
+ )
22
+ @conn = conn
23
+ @columns_hash = {}
24
+ @indexes = []
25
+ @foreign_keys = []
26
+ @primary_keys = nil
27
+ @temporary = temporary
28
+ @if_not_exists = if_not_exists
29
+ @options = options
30
+ @as = as
31
+ @name = name
32
+ @comment = comment
33
+ @view = view || materialized
34
+ @materialized = materialized
35
+ end
36
+
8
37
  def integer(*args, **options)
9
38
  if options[:limit] == 8
10
39
  args.each { |name| column(name, :big_integer, options.except(:limit)) }
@@ -67,6 +67,26 @@ module ActiveRecord
67
67
  end
68
68
  end
69
69
 
70
+ def assume_migrated_upto_version(version, migrations_paths = nil)
71
+ version = version.to_i
72
+ sm_table = quote_table_name(schema_migration.table_name)
73
+
74
+ migrated = migration_context.get_all_versions
75
+ versions = migration_context.migrations.map(&:version)
76
+
77
+ unless migrated.include?(version)
78
+ exec_insert "INSERT INTO #{sm_table} (version) VALUES (#{quote(version.to_s)})", nil, nil
79
+ end
80
+
81
+ inserting = (versions - migrated).select { |v| v < version }
82
+ if inserting.any?
83
+ if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
84
+ raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
85
+ end
86
+ execute insert_versions_sql(inserting)
87
+ end
88
+ end
89
+
70
90
  private
71
91
 
72
92
  def apply_format(sql, format)
@@ -95,11 +115,7 @@ module ActiveRecord
95
115
  end
96
116
 
97
117
  def create_table_definition(*args)
98
- if ActiveRecord::version >= Gem::Version.new('6')
99
- Clickhouse::TableDefinition.new(self, *args)
100
- else
101
- Clickhouse::TableDefinition.new(*args)
102
- end
118
+ Clickhouse::TableDefinition.new(self, *args)
103
119
  end
104
120
 
105
121
  def new_column_from_field(table_name, field)
@@ -140,6 +156,8 @@ module ActiveRecord
140
156
  when "true".freeze, "false".freeze
141
157
  default
142
158
  # Object identifier types
159
+ when "''"
160
+ ''
143
161
  when /\A-?\d+\z/
144
162
  $1
145
163
  else
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'clickhouse-activerecord/arel/visitors/to_sql'
4
4
  require 'clickhouse-activerecord/arel/table'
5
+ require 'clickhouse-activerecord/migration'
5
6
  require 'active_record/connection_adapters/clickhouse/oid/date'
6
7
  require 'active_record/connection_adapters/clickhouse/oid/date_time'
7
8
  require 'active_record/connection_adapters/clickhouse/oid/big_integer'
@@ -110,17 +111,17 @@ module ActiveRecord
110
111
 
111
112
  # Support SchemaMigration from v5.2.2 to v6+
112
113
  def schema_migration # :nodoc:
113
- if ActiveRecord::version >= Gem::Version.new('6')
114
- super
115
- else
116
- ActiveRecord::SchemaMigration
117
- end
114
+ ClickhouseActiverecord::SchemaMigration
118
115
  end
119
116
 
120
117
  def migrations_paths
121
118
  @full_config[:migrations_paths] || 'db/migrate_clickhouse'
122
119
  end
123
120
 
121
+ def migration_context # :nodoc:
122
+ ClickhouseActiverecord::MigrationContext.new(migrations_paths, schema_migration)
123
+ end
124
+
124
125
  def arel_visitor # :nodoc:
125
126
  ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
126
127
  end
@@ -204,17 +205,34 @@ module ActiveRecord
204
205
  def create_database(name)
205
206
  sql = apply_cluster "CREATE DATABASE #{quote_table_name(name)}"
206
207
  log_with_debug(sql, adapter_name) do
207
- res = @connection.post("/?#{@config.except(:database).to_param}", "CREATE DATABASE #{quote_table_name(name)}")
208
+ res = @connection.post("/?#{@config.except(:database).to_param}", sql)
208
209
  process_response(res)
209
210
  end
210
211
  end
211
212
 
212
- def create_table(table_name, comment: nil, **options)
213
- super(
214
- apply_cluster(table_name),
215
- comment: comment,
216
- **options
217
- )
213
+ def create_view(table_name, **options)
214
+ options.merge!(view: true)
215
+ options = apply_replica(table_name, options)
216
+ td = create_table_definition(apply_cluster(table_name), options)
217
+ yield td if block_given?
218
+
219
+ if options[:force]
220
+ drop_table(table_name, options.merge(if_exists: true))
221
+ end
222
+
223
+ execute schema_creation.accept td
224
+ end
225
+
226
+ def create_table(table_name, **options)
227
+ options = apply_replica(table_name, options)
228
+ td = create_table_definition(apply_cluster(table_name), options)
229
+ yield td if block_given?
230
+
231
+ if options[:force]
232
+ drop_table(table_name, options.merge(if_exists: true))
233
+ end
234
+
235
+ execute schema_creation.accept td
218
236
  end
219
237
 
220
238
  # Drops a ClickHouse database.
@@ -226,28 +244,71 @@ module ActiveRecord
226
244
  end
227
245
  end
228
246
 
247
+ def rename_table(table_name, new_name)
248
+ do_execute apply_cluster "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
249
+ end
250
+
229
251
  def drop_table(table_name, options = {}) # :nodoc:
230
252
  do_execute apply_cluster "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
231
253
  end
232
254
 
255
+ def change_column(table_name, column_name, type, options = {})
256
+ result = do_execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}"
257
+ raise "Error parse json response: #{result}" if result.presence && !result.is_a?(Hash)
258
+ end
259
+
260
+ def change_column_null(table_name, column_name, null, default = nil)
261
+ structure = table_structure(table_name).select{|v| v[0] == column_name.to_s}.first
262
+ raise "Column #{column_name} not found in table #{table_name}" if structure.nil?
263
+ change_column table_name, column_name, structure[1].gsub(/(Nullable\()?(.*?)\)?/, '\2'), {null: null, default: default}.compact
264
+ end
265
+
266
+ def change_column_default(table_name, column_name, default)
267
+ change_column table_name, column_name, nil, {default: default}.compact
268
+ end
269
+
270
+ def cluster
271
+ @full_config[:cluster_name]
272
+ end
273
+
274
+ def replica
275
+ @full_config[:replica_name]
276
+ end
277
+
278
+ def replica_path(table)
279
+ "/clickhouse/tables/#{cluster}/#{@config[:database]}.#{table}"
280
+ end
281
+
282
+ def apply_cluster(sql)
283
+ cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
284
+ end
285
+
233
286
  protected
234
287
 
235
288
  def last_inserted_id(result)
236
289
  result
237
290
  end
238
291
 
292
+ def change_column_for_alter(table_name, column_name, type, options = {})
293
+ td = create_table_definition(table_name)
294
+ cd = td.new_column_definition(column_name, type, options)
295
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))
296
+ end
297
+
239
298
  private
240
299
 
241
300
  def connect
242
301
  @connection = Net::HTTP.start(@connection_parameters[0], @connection_parameters[1], use_ssl: @connection_parameters[2], verify_mode: OpenSSL::SSL::VERIFY_NONE)
243
302
  end
244
303
 
245
- def cluster
246
- @full_config[:cluster]
247
- end
248
-
249
- def apply_cluster(sql)
250
- cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
304
+ def apply_replica(table, options)
305
+ if replica && cluster && options[:options]
306
+ match = options[:options].match(/^(.*?MergeTree)\(([^\)]*)\)(.*?)$/)
307
+ if match
308
+ options[:options] = "Replicated#{match[1]}(#{([replica_path(table), replica].map{|v| "'#{v}'"} + [match[2].presence]).compact.join(', ')})#{match[3]}"
309
+ end
310
+ end
311
+ options
251
312
  end
252
313
  end
253
314
  end
@@ -4,6 +4,7 @@ require 'active_record/connection_adapters/clickhouse_adapter'
4
4
 
5
5
  if defined?(Rails::Railtie)
6
6
  require 'clickhouse-activerecord/railtie'
7
+ require 'clickhouse-activerecord/schema'
7
8
  require 'clickhouse-activerecord/schema_dumper'
8
9
  require 'clickhouse-activerecord/tasks'
9
10
  ActiveRecord::Tasks::DatabaseTasks.register_task(/clickhouse/, "ClickhouseActiverecord::Tasks")
@@ -0,0 +1,92 @@
1
+ module ClickhouseActiverecord
2
+
3
+ class SchemaMigration < ::ActiveRecord::SchemaMigration
4
+ class << self
5
+
6
+ def create_table
7
+ unless table_exists?
8
+ version_options = connection.internal_string_options_for_primary_key
9
+
10
+ connection.create_table(table_name, id: false, options: 'ReplacingMergeTree(ver) PARTITION BY version ORDER BY (version)') do |t|
11
+ t.string :version, version_options
12
+ t.column :active, 'Int8', null: false, default: '1'
13
+ t.datetime :ver, null: false, default: -> { 'now()' }
14
+ end
15
+ end
16
+ end
17
+
18
+ def all_versions
19
+ from("#{table_name} FINAL").where(active: 1).order(:version).pluck(:version)
20
+ end
21
+ end
22
+ end
23
+
24
+ class InternalMetadata < ::ActiveRecord::InternalMetadata
25
+ class << self
26
+ def create_table
27
+ unless table_exists?
28
+ key_options = connection.internal_string_options_for_primary_key
29
+
30
+ connection.create_table(table_name, id: false, options: 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)') do |t|
31
+ t.string :key, key_options
32
+ t.string :value
33
+ t.timestamps
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class MigrationContext < ::ActiveRecord::MigrationContext #:nodoc:
41
+ attr_reader :migrations_paths, :schema_migration
42
+
43
+ def initialize(migrations_paths, schema_migration)
44
+ @migrations_paths = migrations_paths
45
+ @schema_migration = schema_migration
46
+ end
47
+
48
+ def down(target_version = nil)
49
+ selected_migrations = if block_given?
50
+ migrations.select { |m| yield m }
51
+ else
52
+ migrations
53
+ end
54
+
55
+ ClickhouseActiverecord::Migrator.new(:down, selected_migrations, schema_migration, target_version).migrate
56
+ end
57
+
58
+ def get_all_versions
59
+ if schema_migration.table_exists?
60
+ schema_migration.all_versions.map(&:to_i)
61
+ else
62
+ []
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ class Migrator < ::ActiveRecord::Migrator
69
+
70
+ def initialize(direction, migrations, schema_migration, target_version = nil)
71
+ @direction = direction
72
+ @target_version = target_version
73
+ @migrated_versions = nil
74
+ @migrations = migrations
75
+ @schema_migration = schema_migration
76
+
77
+ validate(@migrations)
78
+
79
+ @schema_migration.create_table
80
+ ClickhouseActiverecord::InternalMetadata.create_table
81
+ end
82
+
83
+ def record_version_state_after_migrating(version)
84
+ if down?
85
+ migrated.delete(version)
86
+ @schema_migration.create!(version: version.to_s, active: 0)
87
+ else
88
+ super
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickhouseActiverecord
4
+
5
+ class Schema < ::ActiveRecord::Schema
6
+
7
+ def define(info, &block) # :nodoc:
8
+ instance_eval(&block)
9
+
10
+ if info[:version].present?
11
+ connection.schema_migration.create_table
12
+ connection.assume_migrated_upto_version(info[:version], ClickhouseActiverecord::Migrator.migrations_paths)
13
+ end
14
+
15
+ ClickhouseActiverecord::InternalMetadata.create_table
16
+ ClickhouseActiverecord::InternalMetadata[:environment] = connection.migration_context.current_environment
17
+ end
18
+ end
19
+ end
@@ -1,10 +1,129 @@
1
1
  module ClickhouseActiverecord
2
2
  class SchemaDumper < ::ActiveRecord::ConnectionAdapters::SchemaDumper
3
3
 
4
+ attr_accessor :simple
5
+
6
+ class << self
7
+ def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base, default = false)
8
+ dumper = connection.create_schema_dumper(generate_options(config))
9
+ dumper.simple = default
10
+ dumper.dump(stream)
11
+ stream
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def header(stream)
18
+ stream.puts <<HEADER
19
+ # This file is auto-generated from the current state of the database. Instead
20
+ # of editing this file, please use the migrations feature of Active Record to
21
+ # incrementally modify your database, and then regenerate this schema definition.
22
+ #
23
+ # This file is the source Rails uses to define your schema when running `rails
24
+ # #{simple ? 'db' : 'clickhouse'}:schema:load`. When creating a new database, `rails #{simple ? 'db' : 'clickhouse'}:schema:load` tends to
25
+ # be faster and is potentially less error prone than running all of your
26
+ # migrations from scratch. Old migrations may fail to apply correctly if those
27
+ # migrations use external dependencies or application code.
28
+ #
29
+ # It's strongly recommended that you check this file into your version control system.
30
+
31
+ #{simple ? 'ActiveRecord' : 'ClickhouseActiverecord'}::Schema.define(#{define_params}) do
32
+
33
+ HEADER
34
+ end
35
+
4
36
  def table(table, stream)
5
- stream.puts " # TABLE: #{table}"
6
- stream.puts " # SQL: #{@connection.do_system_execute("SHOW CREATE TABLE `#{table.gsub(/^\.inner\./, '')}`")['data'].try(:first).try(:first)}"
7
- super(table.gsub(/^\.inner\./, ''), stream)
37
+ if table.match(/^\.inner\./).nil?
38
+ unless simple
39
+ stream.puts " # TABLE: #{table}"
40
+ sql = @connection.do_system_execute("SHOW CREATE TABLE `#{table.gsub(/^\.inner\./, '')}`")['data'].try(:first).try(:first)
41
+ stream.puts " # SQL: #{sql.gsub(/ENGINE = Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "ENGINE = \\1(\\2)")}" if sql
42
+ # super(table.gsub(/^\.inner\./, ''), stream)
43
+
44
+ # detect view table
45
+ match = sql.match(/^CREATE\s+(MATERIALIZED)\s+VIEW/)
46
+ end
47
+
48
+ # Copy from original dumper
49
+ columns = @connection.columns(table)
50
+ begin
51
+ tbl = StringIO.new
52
+
53
+ # first dump primary key column
54
+ pk = @connection.primary_key(table)
55
+
56
+ tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
57
+
58
+ unless simple
59
+ # Add materialize flag
60
+ tbl.print ', view: true' if match
61
+ tbl.print ', materialized: true' if match && match[1].presence
62
+ end
63
+
64
+ case pk
65
+ when String
66
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
67
+ pkcol = columns.detect { |c| c.name == pk }
68
+ pkcolspec = column_spec_for_primary_key(pkcol)
69
+ if pkcolspec.present?
70
+ tbl.print ", #{format_colspec(pkcolspec)}"
71
+ end
72
+ when Array
73
+ tbl.print ", primary_key: #{pk.inspect}"
74
+ else
75
+ tbl.print ", id: false"
76
+ end
77
+
78
+ unless simple
79
+ table_options = @connection.table_options(table)
80
+ if table_options.present?
81
+ tbl.print ", #{format_options(table_options)}"
82
+ end
83
+ end
84
+
85
+ tbl.puts ", force: :cascade do |t|"
86
+
87
+ # then dump all non-primary key columns
88
+ if simple || !match
89
+ columns.each do |column|
90
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
91
+ next if column.name == pk
92
+ type, colspec = column_spec(column)
93
+ tbl.print " t.#{type} #{column.name.inspect}"
94
+ tbl.print ", #{format_colspec(colspec)}" if colspec.present?
95
+ tbl.puts
96
+ end
97
+ end
98
+
99
+ indexes_in_create(table, tbl)
100
+
101
+ tbl.puts " end"
102
+ tbl.puts
103
+
104
+ tbl.rewind
105
+ stream.print tbl.read
106
+ rescue => e
107
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
108
+ stream.puts "# #{e.message}"
109
+ stream.puts
110
+ end
111
+ end
112
+ end
113
+
114
+ def format_options(options)
115
+ if options && options[:options]
116
+ options[:options] = options[:options].gsub(/^Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "\\1(\\2)")
117
+ end
118
+ super
119
+ end
120
+
121
+ def format_colspec(colspec)
122
+ if simple
123
+ super.gsub(/CAST\('?([^,']*)'?,\s?'.*?'\)/, "\\1")
124
+ else
125
+ super
126
+ end
8
127
  end
9
128
  end
10
129
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '0.3.13'
2
+ VERSION = '0.4.6'
3
3
  end
@@ -12,8 +12,8 @@ class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerato
12
12
  private
13
13
 
14
14
  def db_migrate_path
15
- if defined?(Rails.application) && Rails.application
16
- configured_migrate_path || default_migrate_path
15
+ if defined?(Rails.application) && Rails.application && respond_to?(:configured_migrate_path, true)
16
+ configured_migrate_path
17
17
  else
18
18
  default_migrate_path
19
19
  end
@@ -3,67 +3,11 @@
3
3
  namespace :clickhouse do
4
4
 
5
5
  task prepare_schema_migration_table: :environment do
6
- cluster, database, replica = ActiveRecord::Base.connection_config.values_at(:cluster, :database, :replica)
7
- return if cluster.nil?
8
-
9
- connection = ActiveRecord::Base.connection
10
- key_options = connection.internal_string_options_for_primary_key
11
- block = Proc.new do |t|
12
- t.string :version, key_options
13
- end
14
- distributed_table_name = ".#{ActiveRecord::SchemaMigration.table_name}_distributed"
15
- unless connection.table_exists?(distributed_table_name)
16
- options = { id: false }
17
- if replica
18
- shard = replica.is_a?(String) ? replica : '{shard}'
19
- options[:options] = <<-SQL
20
- ReplicatedMergeTree('/clickhouse/tables/{cluster}/#{shard}/#{database}.`#{distributed_table_name}`', '{replica}')
21
- PARTITION BY version ORDER BY (version) SETTINGS index_granularity = 8192
22
- SQL
23
- end
24
- connection.create_table("`#{distributed_table_name}`", options, &block)
25
- end
26
- unless connection.table_exists?(ActiveRecord::SchemaMigration.table_name)
27
- connection.create_table(
28
- ActiveRecord::SchemaMigration.table_name,
29
- id: false,
30
- options: "Distributed(#{cluster},#{database},`#{distributed_table_name}`,sipHash64(version))",
31
- &block
32
- )
33
- end
6
+ ClickhouseActiverecord::SchemaMigration.create_table unless ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any?
34
7
  end
35
8
 
36
9
  task prepare_internal_metadata_table: :environment do
37
- cluster, database, replica = ActiveRecord::Base.connection_config.values_at(:cluster, :database, :replica)
38
- return if cluster.nil?
39
-
40
- connection = ActiveRecord::Base.connection
41
- key_options = connection.internal_string_options_for_primary_key
42
- block = Proc.new do |t|
43
- t.string :key, key_options
44
- t.string :value
45
- t.timestamps
46
- end
47
- distributed_table_name = ".#{ActiveRecord::InternalMetadata.table_name}_distributed"
48
- unless connection.table_exists?(distributed_table_name)
49
- options = { id: false }
50
- if replica
51
- shard = replica.is_a?(String) ? replica : '{shard}'
52
- options[:options] = <<-SQL
53
- ReplicatedMergeTree('/clickhouse/tables/{cluster}/#{shard}/#{database}.`#{distributed_table_name}`', '{replica}')
54
- PARTITION BY toDate(created_at) ORDER BY (created_at) SETTINGS index_granularity = 8192
55
- SQL
56
- end
57
- connection.create_table("`#{distributed_table_name}`", options, &block)
58
- end
59
- unless connection.table_exists?(ActiveRecord::InternalMetadata.table_name)
60
- connection.create_table(
61
- ActiveRecord::InternalMetadata.table_name,
62
- id: false,
63
- options: "Distributed(#{cluster},#{database},`#{distributed_table_name}`,sipHash64(created_at))",
64
- &block
65
- )
66
- end
10
+ ClickhouseActiverecord::InternalMetadata.create_table unless ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any?
67
11
  end
68
12
 
69
13
  task load_config: :environment do
@@ -76,16 +20,19 @@ namespace :clickhouse do
76
20
 
77
21
  # todo not testing
78
22
  desc 'Load database schema'
79
- task load: :load_config do
80
- load("#{Rails.root}/db/clickhouse_schema.rb")
23
+ task load: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do |t, args|
24
+ simple = ENV['simple'] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? ? '_simple' : nil
25
+ ClickhouseActiverecord::SchemaMigration.drop_table
26
+ load("#{Rails.root}/db/clickhouse_schema#{simple}.rb")
81
27
  end
82
28
 
83
29
  desc 'Dump database schema'
84
- task dump: :environment do
85
- filename = "#{Rails.root}/db/clickhouse_schema.rb"
30
+ task dump: :environment do |t, args|
31
+ simple = ENV['simple'] || args[:simple] || ARGV.map{|a| a.include?('--simple') ? true : nil}.compact.any? ? '_simple' : nil
32
+ filename = "#{Rails.root}/db/clickhouse_schema#{simple}.rb"
86
33
  File.open(filename, 'w:utf-8') do |file|
87
34
  ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
88
- ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
35
+ ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file, ActiveRecord::Base, !!simple)
89
36
  end
90
37
  end
91
38
 
@@ -127,5 +74,13 @@ namespace :clickhouse do
127
74
  desc 'Migrate the clickhouse database'
128
75
  task migrate: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do
129
76
  Rake::Task['db:migrate'].execute
77
+ if File.exists? "#{Rails.root}/db/clickhouse_schema_simple.rb"
78
+ Rake::Task['clickhouse:schema:dump'].execute(simple: true)
79
+ end
80
+ end
81
+
82
+ desc 'Rollback the clickhouse database'
83
+ task rollback: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do
84
+ Rake::Task['db:rollback'].execute
130
85
  end
131
86
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clickhouse-activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-11 00:00:00.000000000 Z
11
+ date: 2020-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '13.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,7 +122,9 @@ files:
122
122
  - lib/clickhouse-activerecord.rb
123
123
  - lib/clickhouse-activerecord/arel/table.rb
124
124
  - lib/clickhouse-activerecord/arel/visitors/to_sql.rb
125
+ - lib/clickhouse-activerecord/migration.rb
125
126
  - lib/clickhouse-activerecord/railtie.rb
127
+ - lib/clickhouse-activerecord/schema.rb
126
128
  - lib/clickhouse-activerecord/schema_dumper.rb
127
129
  - lib/clickhouse-activerecord/tasks.rb
128
130
  - lib/clickhouse-activerecord/version.rb