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 +5 -5
- data/CHANGELOG.md +14 -4
- data/README.md +81 -11
- data/clickhouse-activerecord.gemspec +1 -1
- data/lib/active_record/connection_adapters/clickhouse/oid/date_time.rb +1 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +20 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_definitions.rb +29 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +36 -42
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +77 -12
- data/lib/clickhouse-activerecord.rb +1 -0
- data/lib/clickhouse-activerecord/migration.rb +92 -0
- data/lib/clickhouse-activerecord/schema.rb +19 -0
- data/lib/clickhouse-activerecord/schema_dumper.rb +93 -3
- data/lib/clickhouse-activerecord/tasks.rb +15 -1
- data/lib/clickhouse-activerecord/version.rb +1 -1
- data/lib/generators/clickhouse_migration_generator.rb +15 -1
- data/lib/tasks/clickhouse.rake +27 -2
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8020d332d2b7b707fa2776a37fbb8d2c217ec50703962d06938be53eaae22796
|
4
|
+
data.tar.gz: 6979255cb79396c6574e4b58c0e82ae39a42612fe1068b1308d98a1222c58b64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3aa6d799adf1fc66d49c3dff6d02c5ad0349e467fb1067a1453de8bfcb3316abb46681367b7dfd3ebd8f82f13460cb658c4f167ff23398bba8b9cc459c5b6df1
|
7
|
+
data.tar.gz: a1c8e252279678efb2fe1f8c36a8a7e1c5bfda24896438245921deee5862e94c047dced14b85408b5e1343606ec7055100b813af399568f5ee405a255c268777
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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', '~>
|
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
|
@@ -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
|
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
|
-
|
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
|
-
|
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
|
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(
|
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(
|
96
|
-
super(
|
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
|
-
|
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
|
-
|
221
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
@@ -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, "
|
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
|
data/lib/tasks/clickhouse.rake
CHANGED
@@ -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.
|
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:
|
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: '
|
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: '
|
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
|
-
|
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
|