clickhouse-activerecord 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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