clickhouse-activerecord 0.3.13 → 0.4.6

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 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