clickhouse-activerecord 0.3.8 → 0.4.0

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
- SHA1:
3
- metadata.gz: 1d313720b1a43f19f51e6fa9e88ef40d136130e9
4
- data.tar.gz: e82dbfb65bd38fa50df9304690bd116eef021a70
2
+ SHA256:
3
+ metadata.gz: 8020d332d2b7b707fa2776a37fbb8d2c217ec50703962d06938be53eaae22796
4
+ data.tar.gz: 6979255cb79396c6574e4b58c0e82ae39a42612fe1068b1308d98a1222c58b64
5
5
  SHA512:
6
- metadata.gz: 8fed861c69428580cddb00c79656ac7614bd26f5c47cdc0a6e4378b0578f4935aec86430b5523f599a97753be329482a8e766471b505d1fd04961ad98ac0415f
7
- data.tar.gz: 581d95bdc952a7be4d5645ea0948011f7c67ca6b145c3607774a9770997382fa36279b6029b4128d43909948658b956db64d0d185351eee1f6f756cbaa36bd7c
6
+ metadata.gz: 3aa6d799adf1fc66d49c3dff6d02c5ad0349e467fb1067a1453de8bfcb3316abb46681367b7dfd3ebd8f82f13460cb658c4f167ff23398bba8b9cc459c5b6df1
7
+ data.tar.gz: a1c8e252279678efb2fe1f8c36a8a7e1c5bfda24896438245921deee5862e94c047dced14b85408b5e1343606ec7055100b813af399568f5ee405a255c268777
@@ -1,3 +1,13 @@
1
+ ### Version 0.4.0 (Sep 18, 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
+
7
+ ### Version 0.3.10 (Dec 20, 2019)
8
+
9
+ * Support structure dump/load [@StoneGod](https://github.com/StoneGod)
10
+
1
11
  ### Version 0.3.6 (Sep 2, 2019)
2
12
 
3
13
  * Support Rails 6.0
@@ -5,18 +15,18 @@
5
15
 
6
16
  ### Version 0.3.4 (Jun 28, 2019)
7
17
 
8
- * Fix DateTime sql format without microseconds for Rails 5.2
18
+ * Fix DateTime sql format without microseconds for Rails 5.2
9
19
  * Support ssl connection
10
20
  * Migration support
11
21
  * Rake tasks for create / drop database
12
-
22
+
13
23
  ### Version 0.3.0 (Nov 27, 2018)
14
24
 
15
25
  * Support materialized view
16
26
  * Aggregated functions for view
17
27
  * Schema dumper with SQL create table
18
28
  * Added migrations support [@Bugagazavr](https://github.com/Bugagazavr)
19
-
29
+
20
30
  ### Version 0.2.0 (Oct 3, 2017)
21
31
 
22
32
  * Support Rails 5.0
@@ -24,7 +34,7 @@
24
34
  ### Version 0.1.2 (Sep 27, 2017)
25
35
 
26
36
  * Fix Big Int type
27
-
37
+
28
38
  ### Version 0.1.0 (Aug 31, 2017)
29
39
 
30
40
  * Initial release
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Clickhouse::Activerecord
2
2
 
3
3
  A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
4
+ Support ClickHouse version from 19.14 LTS.
4
5
 
5
6
  ## Installation
6
7
 
@@ -17,8 +18,24 @@ And then execute:
17
18
  Or install it yourself as:
18
19
 
19
20
  $ gem install clickhouse-activerecord
21
+
22
+ ## Available database connection parameters
23
+ ```yml
24
+ default: &default
25
+ adapter: clickhouse
26
+ database: database
27
+ host: localhost
28
+ port: 8123
29
+ username: username
30
+ password: password
31
+ ssl: true # optional for using ssl connection
32
+ debug: true # use for showing in to log technical information
33
+ migrations_paths: db/clickhouse # optional, default: db/migrate_clickhouse
34
+ cluster_name: 'cluster_name' # optional for creating tables in cluster
35
+ replica_name: '{replica}' # replica macros name, optional for creating replicated tables
36
+ ```
20
37
 
21
- ## Usage
38
+ ## Usage in Rails 5
22
39
 
23
40
  Add your `database.yml` connection information with postfix `_clickhouse` for you environment:
24
41
 
@@ -26,10 +43,6 @@ Add your `database.yml` connection information with postfix `_clickhouse` for yo
26
43
  development_clickhouse:
27
44
  adapter: clickhouse
28
45
  database: database
29
- host: localhost
30
- username: username
31
- password: password
32
- debug: true # use for showing in to log technical information
33
46
  ```
34
47
 
35
48
  Add to your model:
@@ -54,26 +67,55 @@ Or global connection:
54
67
  development:
55
68
  adapter: clickhouse
56
69
  database: database
57
- host: localhost
58
- username: username
59
- password: password
70
+ ```
71
+
72
+ ## Usage in Rails 6 with second database
73
+
74
+ Add your `database.yml` connection information for you environment:
75
+
76
+ ```yml
77
+ development:
78
+ primary:
79
+ ...
80
+
81
+ clickhouse:
82
+ adapter: clickhouse
83
+ database: database
84
+ ```
85
+
86
+ Connection [Multiple Databases with Active Record](https://guides.rubyonrails.org/active_record_multiple_databases.html) or short example:
87
+
88
+ ```ruby
89
+ class Action < ActiveRecord::Base
90
+ connects_to database: { writing: :clickhouse, reading: :clickhouse }
91
+ end
60
92
  ```
61
93
 
62
94
  ### Rake tasks
63
95
 
96
+ **Note!** For Rails 6 you can use default rake tasks if you configure `migrations_paths` in your `database.yml`, for example: `rake db:migrate`
97
+
64
98
  Create / drop / purge / reset database:
65
99
 
66
100
  $ rake clickhouse:create
67
101
  $ rake clickhouse:drop
68
102
  $ rake clickhouse:purge
69
103
  $ rake clickhouse:reset
104
+
105
+ Prepare system tables for rails:
106
+
107
+ $ rake clickhouse:prepare_schema_migration_table
108
+ $ rake clickhouse:prepare_internal_metadata_table
70
109
 
71
110
  Migration:
72
111
 
73
112
  $ rails g clickhouse_migration MIGRATION_NAME COLUMNS
74
113
  $ rake clickhouse:migrate
75
-
76
- Rollback migration not supported!
114
+ $ rake clickhouse:rollback
115
+
116
+ ### Dump / Load for multiple using databases
117
+
118
+ If you using multiple databases, for example: PostgreSQL, Clickhouse.
77
119
 
78
120
  Schema dump to `db/clickhouse_schema.rb` file:
79
121
 
@@ -82,9 +124,24 @@ Schema dump to `db/clickhouse_schema.rb` file:
82
124
  Schema load from `db/clickhouse_schema.rb` file:
83
125
 
84
126
  $ rake clickhouse:schema:load
85
-
127
+
86
128
  We use schema for emulate development or tests environment on PostgreSQL adapter.
87
129
 
130
+ Structure dump to `db/clickhouse_structure.sql` file:
131
+
132
+ $ rake clickhouse:structure:dump
133
+
134
+ Structure load from `db/clickhouse_structure.sql` file:
135
+
136
+ $ rake clickhouse:structure:load
137
+
138
+ ### Dump / Load for only Clickhouse database using
139
+
140
+ $ rake db:schema:dump
141
+ $ rake db:schema:load
142
+ $ rake db:structure:dump
143
+ $ rake db:structure:load
144
+
88
145
  ### Insert and select data
89
146
 
90
147
  ```ruby
@@ -101,6 +158,19 @@ ActionView.maximum(:date)
101
158
  #=> 'Wed, 29 Nov 2017'
102
159
  ```
103
160
 
161
+ ### Using replica and cluster params in connection parameters
162
+
163
+ ```yml
164
+ default: &default
165
+ ***
166
+ cluster_name: 'cluster_name'
167
+ replica_name: '{replica}'
168
+ ```
169
+
170
+ `ON CLUSTER cluster_name` will be attach to all queries create / drop.
171
+
172
+ Engines `MergeTree` and all support replication engines will be replaced to `Replicated***('/clickhouse/tables/cluster_name/database.table', '{replica}')`
173
+
104
174
  ## Donations
105
175
 
106
176
  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
@@ -8,6 +8,7 @@ module ActiveRecord
8
8
 
9
9
  def serialize(value)
10
10
  value = super
11
+ return unless value
11
12
  return value.strftime('%Y-%m-%d %H:%M:%S') unless value.acts_like?(:time)
12
13
 
13
14
  value.to_time.strftime('%Y-%m-%d %H:%M:%S')
@@ -27,6 +27,26 @@ module ActiveRecord
27
27
 
28
28
  create_sql
29
29
  end
30
+
31
+ def visit_TableDefinition(o)
32
+ create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} "
33
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
34
+ create_sql << "#{quote_table_name(o.name)} "
35
+
36
+ statements = o.columns.map { |c| accept c }
37
+ statements << accept(o.primary_keys) if o.primary_keys
38
+
39
+ create_sql << "(#{statements.join(', ')})" if statements.present?
40
+ add_table_options!(create_sql, table_options(o))
41
+ create_sql << " AS #{to_sql(o.as)}" if o.as
42
+ create_sql
43
+ end
44
+
45
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
46
+ def table_modifier_in_create(o)
47
+ " TEMPORARY" if o.temporary
48
+ " MATERIALIZED" if o.materialized
49
+ end
30
50
  end
31
51
  end
32
52
  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 = @conn.apply_cluster(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)) }
@@ -17,6 +17,8 @@ module ActiveRecord
17
17
  def exec_query(sql, name = nil, binds = [], prepare: false)
18
18
  result = do_execute(sql, name)
19
19
  ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
20
+ rescue StandardError => _e
21
+ raise ActiveRecord::ActiveRecordError, "Response: #{result}"
20
22
  end
21
23
 
22
24
  def exec_update(_sql, _name = nil, _binds = [])
@@ -34,41 +36,10 @@ module ActiveRecord
34
36
  end
35
37
 
36
38
  def table_options(table)
37
- sql = do_system_execute("SHOW CREATE TABLE #{table}")['data'].try(:first).try(:first)
39
+ sql = do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first)
38
40
  { options: sql.gsub(/^(?:.*?)ENGINE = (.*?)$/, '\\1') }
39
41
  end
40
42
 
41
- # @todo copied from ActiveRecord::ConnectionAdapters::SchemaStatements v5.2.2
42
- # Why version column type of String, but insert to Integer?
43
- def assume_migrated_upto_version(version, migrations_paths)
44
- migrations_paths = Array(migrations_paths)
45
- version = version.to_i
46
- sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
47
-
48
- migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i)
49
- versions = migration_context.migration_files.map do |file|
50
- migration_context.parse_migration_filename(file).first.to_i
51
- end
52
-
53
- unless migrated.include?(version)
54
- do_execute( "INSERT INTO #{sm_table} (version) VALUES (#{quote(version.to_s)})", 'SchemaMigration', format: nil)
55
- end
56
-
57
- inserting = (versions - migrated).select { |v| v < version }
58
- if inserting.any?
59
- if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
60
- raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
61
- end
62
- if supports_multi_insert?
63
- do_system_execute insert_versions_sql(inserting.map(&:to_s))
64
- else
65
- inserting.each do |v|
66
- do_system_execute insert_versions_sql(v)
67
- end
68
- end
69
- end
70
- end
71
-
72
43
  # Not indexes on clickhouse
73
44
  def indexes(table_name, name = nil)
74
45
  []
@@ -86,21 +57,42 @@ module ActiveRecord
86
57
  end
87
58
  end
88
59
 
89
- private
90
-
91
- def apply_format(sql, format)
92
- format ? "#{sql} FORMAT #{format}" : sql
93
- end
94
-
95
- def do_execute(sql, name = nil, format: 'JSONCompact')
60
+ def do_execute(sql, name = nil, format: 'JSONCompact', settings: {})
96
61
  log(sql, "#{adapter_name} #{name}") do
97
62
  formatted_sql = apply_format(sql, format)
98
- res = @connection.post("/?#{@config.to_param}", formatted_sql)
63
+ request_params = @config || {}
64
+ res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql)
99
65
 
100
66
  process_response(res)
101
67
  end
102
68
  end
103
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
+
90
+ private
91
+
92
+ def apply_format(sql, format)
93
+ format ? "#{sql} FORMAT #{format}" : sql
94
+ end
95
+
104
96
  def process_response(res)
105
97
  case res.code.to_i
106
98
  when 200
@@ -109,6 +101,8 @@ module ActiveRecord
109
101
  raise ActiveRecord::ActiveRecordError,
110
102
  "Response code: #{res.code}:\n#{res.body}"
111
103
  end
104
+ rescue JSON::ParserError
105
+ res.body
112
106
  end
113
107
 
114
108
  def log_with_debug(sql, name = nil)
@@ -121,7 +115,7 @@ module ActiveRecord
121
115
  end
122
116
 
123
117
  def create_table_definition(*args)
124
- Clickhouse::TableDefinition.new(*args)
118
+ Clickhouse::TableDefinition.new(self, *args)
125
119
  end
126
120
 
127
121
  def new_column_from_field(table_name, field)
@@ -140,7 +134,7 @@ module ActiveRecord
140
134
  protected
141
135
 
142
136
  def table_structure(table_name)
143
- result = do_system_execute("DESCRIBE TABLE #{table_name}", table_name)
137
+ result = do_system_execute("DESCRIBE TABLE `#{table_name}`", table_name)
144
138
  data = result['data']
145
139
 
146
140
  return data unless data.empty?
@@ -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'
@@ -18,6 +19,7 @@ module ActiveRecord
18
19
  config = config.symbolize_keys
19
20
  host = config[:host] || 'localhost'
20
21
  port = config[:port] || 8123
22
+ ssl = config[:ssl].present? ? config[:ssl] : port == 443
21
23
 
22
24
  if config.key?(:database)
23
25
  database = config[:database]
@@ -25,7 +27,7 @@ module ActiveRecord
25
27
  raise ArgumentError, 'No database specified. Missing argument: database.'
26
28
  end
27
29
 
28
- ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact, config[:debug])
30
+ ConnectionAdapters::ClickhouseAdapter.new(logger, [host, port, ssl], { user: config[:username], password: config[:password], database: database }.compact, config)
29
31
  end
30
32
  end
31
33
  end
@@ -92,21 +94,34 @@ module ActiveRecord
92
94
  include Clickhouse::SchemaStatements
93
95
 
94
96
  # Initializes and connects a Clickhouse adapter.
95
- def initialize(connection, logger, connection_parameters, config, debug = false)
96
- super(connection, logger)
97
+ def initialize(logger, connection_parameters, config, full_config)
98
+ super(nil, logger)
97
99
  @connection_parameters = connection_parameters
98
100
  @config = config
99
- @debug = debug
101
+ @debug = full_config[:debug] || false
102
+ @full_config = full_config
100
103
 
101
- if ActiveRecord::version >= Gem::Version.new('6')
104
+ @prepared_statements = false
105
+ if ActiveRecord::version == Gem::Version.new('6.0.0')
102
106
  @prepared_statement_status = Concurrent::ThreadLocalVar.new(false)
103
- else
104
- @prepared_statements = false
105
107
  end
106
108
 
107
109
  connect
108
110
  end
109
111
 
112
+ # Support SchemaMigration from v5.2.2 to v6+
113
+ def schema_migration # :nodoc:
114
+ ClickhouseActiverecord::SchemaMigration
115
+ end
116
+
117
+ def migrations_paths
118
+ @full_config[:migrations_paths] || 'db/migrate_clickhouse'
119
+ end
120
+
121
+ def migration_context # :nodoc:
122
+ ClickhouseActiverecord::MigrationContext.new(migrations_paths, schema_migration)
123
+ end
124
+
110
125
  def arel_visitor # :nodoc:
111
126
  ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
112
127
  end
@@ -188,16 +203,41 @@ module ActiveRecord
188
203
 
189
204
  # Create a new ClickHouse database.
190
205
  def create_database(name)
191
- sql = "CREATE DATABASE #{quote_table_name(name)}"
206
+ sql = apply_cluster "CREATE DATABASE #{quote_table_name(name)}"
192
207
  log_with_debug(sql, adapter_name) do
193
208
  res = @connection.post("/?#{@config.except(:database).to_param}", "CREATE DATABASE #{quote_table_name(name)}")
194
209
  process_response(res)
195
210
  end
196
211
  end
197
212
 
213
+ def create_view(table_name, **options)
214
+ options.merge!(view: true)
215
+ options = apply_replica(table_name, options)
216
+ td = create_table_definition(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(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
236
+ end
237
+
198
238
  # Drops a ClickHouse database.
199
239
  def drop_database(name) #:nodoc:
200
- sql = "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
240
+ sql = apply_cluster "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
201
241
  log_with_debug(sql, adapter_name) do
202
242
  res = @connection.post("/?#{@config.except(:database).to_param}", sql)
203
243
  process_response(res)
@@ -205,7 +245,23 @@ module ActiveRecord
205
245
  end
206
246
 
207
247
  def drop_table(table_name, options = {}) # :nodoc:
208
- do_execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
248
+ do_execute apply_cluster "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
249
+ end
250
+
251
+ def cluster
252
+ @full_config[:cluster_name]
253
+ end
254
+
255
+ def replica
256
+ @full_config[:replica_name]
257
+ end
258
+
259
+ def replica_path(table)
260
+ "/clickhouse/tables/#{cluster}/#{@config[:database]}.#{table}"
261
+ end
262
+
263
+ def apply_cluster(sql)
264
+ cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
209
265
  end
210
266
 
211
267
  protected
@@ -217,8 +273,17 @@ module ActiveRecord
217
273
  private
218
274
 
219
275
  def connect
220
- # for ssl port need use ssl
221
- @connection = Net::HTTP.start(@connection_parameters[0], @connection_parameters[1], use_ssl: @connection_parameters[1] == 443)
276
+ @connection = Net::HTTP.start(@connection_parameters[0], @connection_parameters[1], use_ssl: @connection_parameters[2], verify_mode: OpenSSL::SSL::VERIFY_NONE)
277
+ end
278
+
279
+ def apply_replica(table, options)
280
+ if replica && cluster
281
+ match = options[:options].match(/^(.*?MergeTree)\(([^\)]*)\)(.*?)$/)
282
+ if match
283
+ options[:options] = "Replicated#{match[1]}(#{([replica_path(table), replica].map{|v| "'#{v}'"} + [match[2].presence]).compact.join(', ')})#{match[3]}"
284
+ end
285
+ end
286
+ options
222
287
  end
223
288
  end
224
289
  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,100 @@
1
1
  module ClickhouseActiverecord
2
2
  class SchemaDumper < ::ActiveRecord::ConnectionAdapters::SchemaDumper
3
3
 
4
+ def header(stream)
5
+ stream.puts <<HEADER
6
+ # This file is auto-generated from the current state of the database. Instead
7
+ # of editing this file, please use the migrations feature of Active Record to
8
+ # incrementally modify your database, and then regenerate this schema definition.
9
+ #
10
+ # This file is the source Rails uses to define your schema when running `rails
11
+ # clickhouse:schema:load`. When creating a new database, `rails clickhouse:schema:load` tends to
12
+ # be faster and is potentially less error prone than running all of your
13
+ # migrations from scratch. Old migrations may fail to apply correctly if those
14
+ # migrations use external dependencies or application code.
15
+ #
16
+ # It's strongly recommended that you check this file into your version control system.
17
+
18
+ ClickhouseActiverecord::Schema.define(#{define_params}) do
19
+
20
+ HEADER
21
+ end
22
+
4
23
  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)
24
+ if table.match(/^\.inner\./).nil?
25
+ stream.puts " # TABLE: #{table}"
26
+ sql = @connection.do_system_execute("SHOW CREATE TABLE `#{table.gsub(/^\.inner\./, '')}`")['data'].try(:first).try(:first)
27
+ stream.puts " # SQL: #{sql.gsub(/ENGINE = Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "ENGINE = \\1(\\2)")}" if sql
28
+ # super(table.gsub(/^\.inner\./, ''), stream)
29
+
30
+ # detect view table
31
+ match = sql.match(/^CREATE\s+(MATERIALIZED)\s+VIEW/)
32
+
33
+ # Copy from original dumper
34
+ columns = @connection.columns(table)
35
+ begin
36
+ tbl = StringIO.new
37
+
38
+ # first dump primary key column
39
+ pk = @connection.primary_key(table)
40
+
41
+ tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
42
+
43
+ # Add materialize flag
44
+ tbl.print ', view: true' if match
45
+ tbl.print ', materialized: true' if match && match[1].presence
46
+
47
+ case pk
48
+ when String
49
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
50
+ pkcol = columns.detect { |c| c.name == pk }
51
+ pkcolspec = column_spec_for_primary_key(pkcol)
52
+ if pkcolspec.present?
53
+ tbl.print ", #{format_colspec(pkcolspec)}"
54
+ end
55
+ when Array
56
+ tbl.print ", primary_key: #{pk.inspect}"
57
+ else
58
+ tbl.print ", id: false"
59
+ end
60
+
61
+ table_options = @connection.table_options(table)
62
+ if table_options.present?
63
+ tbl.print ", #{format_options(table_options)}"
64
+ end
65
+
66
+ tbl.puts ", force: :cascade do |t|"
67
+
68
+ # then dump all non-primary key columns
69
+ columns.each do |column|
70
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
71
+ next if column.name == pk
72
+ type, colspec = column_spec(column)
73
+ tbl.print " t.#{type} #{column.name.inspect}"
74
+ tbl.print ", #{format_colspec(colspec)}" if colspec.present?
75
+ tbl.puts
76
+ end
77
+
78
+ indexes_in_create(table, tbl)
79
+
80
+ tbl.puts " end"
81
+ tbl.puts
82
+
83
+ tbl.rewind
84
+ stream.print tbl.read
85
+ rescue => e
86
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
87
+ stream.puts "# #{e.message}"
88
+ stream.puts
89
+ end
90
+ end
91
+ end
92
+
93
+ def format_options(options)
94
+ if options && options[:options]
95
+ options[:options] = options[:options].gsub(/^Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "\\1(\\2)")
96
+ end
97
+ super
8
98
  end
9
99
  end
10
100
  end
@@ -31,13 +31,27 @@ module ClickhouseActiverecord
31
31
  create
32
32
  end
33
33
 
34
+ def structure_dump(*args)
35
+ tables = connection.execute("SHOW TABLES FROM #{@configuration['database']}")['data'].flatten
36
+
37
+ File.open(args.first, 'w:utf-8') do |file|
38
+ tables.each do |table|
39
+ next if table.match(/\.inner/)
40
+ file.puts connection.execute("SHOW CREATE TABLE #{table}")['data'].try(:first).try(:first).gsub("#{@configuration['database']}.", '') + ";\n\n"
41
+ end
42
+ end
43
+ end
44
+
45
+ def structure_load(*args)
46
+ File.read(args.first).split(";\n\n").each { |sql| connection.execute(sql) }
47
+ end
48
+
34
49
  def migrate
35
50
  check_target_version
36
51
 
37
52
  verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
38
53
  scope = ENV["SCOPE"]
39
54
  verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, verbose
40
- binding.pry
41
55
  connection.migration_context.migrate(target_version) do |migration|
42
56
  scope.blank? || scope == migration.scope
43
57
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '0.3.8'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -6,6 +6,20 @@ class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerato
6
6
  def create_migration_file
7
7
  set_local_assigns!
8
8
  validate_file_name!
9
- migration_template @migration_template, "db/migrate_clickhouse/#{file_name}.rb"
9
+ migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb")
10
+ end
11
+
12
+ private
13
+
14
+ def db_migrate_path
15
+ if defined?(Rails.application) && Rails.application && respond_to?(:configured_migrate_path, true)
16
+ configured_migrate_path
17
+ else
18
+ default_migrate_path
19
+ end
20
+ end
21
+
22
+ def default_migrate_path
23
+ "db/migrate_clickhouse"
10
24
  end
11
25
  end
@@ -2,6 +2,14 @@
2
2
 
3
3
  namespace :clickhouse do
4
4
 
5
+ task prepare_schema_migration_table: :environment do
6
+ ClickhouseActiverecord::SchemaMigration.create_table
7
+ end
8
+
9
+ task prepare_internal_metadata_table: :environment do
10
+ ClickhouseActiverecord::InternalMetadata.create_table
11
+ end
12
+
5
13
  task load_config: :environment do
6
14
  ENV['SCHEMA'] = "db/clickhouse_schema.rb"
7
15
  ActiveRecord::Migrator.migrations_paths = ["db/migrate_clickhouse"]
@@ -12,7 +20,7 @@ namespace :clickhouse do
12
20
 
13
21
  # todo not testing
14
22
  desc 'Load database schema'
15
- task load: :load_config do
23
+ task load: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do
16
24
  load("#{Rails.root}/db/clickhouse_schema.rb")
17
25
  end
18
26
 
@@ -27,6 +35,18 @@ namespace :clickhouse do
27
35
 
28
36
  end
29
37
 
38
+ namespace :structure do
39
+ desc 'Load database structure'
40
+ task load: [:load_config, 'db:check_protected_environments'] do
41
+ ClickhouseActiverecord::Tasks.new(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]).structure_load("#{Rails.root}/db/clickhouse_structure.sql")
42
+ end
43
+
44
+ desc 'Dump database structure'
45
+ task dump: [:load_config, 'db:check_protected_environments'] do
46
+ ClickhouseActiverecord::Tasks.new(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"]).structure_dump("#{Rails.root}/db/clickhouse_structure.sql")
47
+ end
48
+ end
49
+
30
50
  desc 'Creates the database from DATABASE_URL or config/database.yml'
31
51
  task create: [:load_config] do
32
52
  ActiveRecord::Tasks::DatabaseTasks.create(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
@@ -49,7 +69,12 @@ namespace :clickhouse do
49
69
  end
50
70
 
51
71
  desc 'Migrate the clickhouse database'
52
- task migrate: :load_config do
72
+ task migrate: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do
53
73
  Rake::Task['db:migrate'].execute
54
74
  end
75
+
76
+ desc 'Rollback the clickhouse database'
77
+ task rollback: [:load_config, :prepare_schema_migration_table, :prepare_internal_metadata_table] do
78
+ Rake::Task['db:rollback'].execute
79
+ end
55
80
  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.8
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-04 00:00:00.000000000 Z
11
+ date: 2020-09-18 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
@@ -147,8 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
149
  - !ruby/object:Gem::Version
148
150
  version: '0'
149
151
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.5.2.3
152
+ rubygems_version: 3.0.1
152
153
  signing_key:
153
154
  specification_version: 4
154
155
  summary: ClickHouse ActiveRecord