clickhouse-activerecord 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +18 -0
- data/README.md +49 -6
- data/clickhouse-activerecord.gemspec +6 -2
- data/lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb +5 -2
- data/lib/active_record/connection_adapters/clickhouse/oid/date.rb +2 -0
- data/lib/active_record/connection_adapters/clickhouse/oid/date_time.rb +9 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +33 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_definitions.rb +19 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +180 -0
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +69 -112
- data/lib/clickhouse-activerecord.rb +8 -1
- data/lib/clickhouse-activerecord/arel/visitors/to_sql.rb +21 -0
- data/lib/{clickhouse → clickhouse-activerecord}/railtie.rb +3 -1
- data/lib/clickhouse-activerecord/schema_dumper.rb +10 -0
- data/lib/clickhouse-activerecord/tasks.rb +65 -0
- data/lib/clickhouse-activerecord/version.rb +3 -0
- data/lib/generators/clickhouse_migration_generator.rb +11 -0
- data/lib/tasks/clickhouse.rake +35 -3
- metadata +45 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a3c9c8eca31fb8287bb89238201bb073223e84af6828fe5331aa4b18b9ed7b2
|
4
|
+
data.tar.gz: 379946b27adc6065995dbafd4fbd65cdbf4a9bebe5e89b2dcb460136b229382a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f8048d478a31674f135a9aebff80a588c31fcfd153b3264fcd28d0c2d36d3dc5785d96056954caa3ab037d33e5e24ff97fd61337058b875baa5f14922abae2d
|
7
|
+
data.tar.gz: 81a6bf828d38da1dfd71fc58bf7408e84407aa1c6156c64412e28247815536b231d79dc84481c417ca75eff563d5e8ebf2d149ac4eba40a717fef9d35abb1450
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
### Version 0.3.0 (Nov 27, 2018)
|
2
|
+
|
3
|
+
* Support materialized view
|
4
|
+
* Aggregated functions for view
|
5
|
+
* Schema dumper with SQL create table
|
6
|
+
* Added migrations support [@Bugagazavr](https://github.com/Bugagazavr)
|
7
|
+
|
8
|
+
### Version 0.2.0 (Oct 3, 2017)
|
9
|
+
|
10
|
+
* Support Rails 5.0
|
11
|
+
|
12
|
+
### Version 0.1.2 (Sep 27, 2017)
|
13
|
+
|
14
|
+
* Fix Big Int type
|
15
|
+
|
16
|
+
### Version 0.1.0 (Aug 31, 2017)
|
17
|
+
|
18
|
+
* Initial release
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Clickhouse::Activerecord
|
2
2
|
|
3
|
-
A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.
|
3
|
+
A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -29,6 +29,7 @@ development_clickhouse:
|
|
29
29
|
host: localhost
|
30
30
|
username: username
|
31
31
|
password: password
|
32
|
+
debug: true # use for showing in to log technical information
|
32
33
|
```
|
33
34
|
|
34
35
|
Add to your model:
|
@@ -39,7 +40,15 @@ class Action < ActiveRecord::Base
|
|
39
40
|
end
|
40
41
|
```
|
41
42
|
|
42
|
-
|
43
|
+
For materialized view model add:
|
44
|
+
```ruby
|
45
|
+
class ActionView < ActiveRecord::Base
|
46
|
+
establish_connection "#{Rails.env}_clickhouse".to_sym
|
47
|
+
self.is_view = true
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Or global connection:
|
43
52
|
|
44
53
|
```yml
|
45
54
|
development:
|
@@ -50,21 +59,55 @@ development:
|
|
50
59
|
password: password
|
51
60
|
```
|
52
61
|
|
53
|
-
|
62
|
+
### Rake tasks
|
63
|
+
|
64
|
+
Create / drop / purge / reset database:
|
65
|
+
|
66
|
+
$ rake clickhouse:create
|
67
|
+
$ rake clickhouse:drop
|
68
|
+
$ rake clickhouse:purge
|
69
|
+
$ rake clickhouse:reset
|
70
|
+
|
71
|
+
Migration:
|
72
|
+
|
73
|
+
$ rails g clickhouse_migration MIGRATION_NAME COLUMNS
|
74
|
+
$ rake clickhouse:migrate
|
75
|
+
|
76
|
+
Rollback migration not supported!
|
77
|
+
|
78
|
+
Schema dump to `db/clickhouse_schema.rb` file:
|
54
79
|
|
55
80
|
$ rake clickhouse:schema:dump
|
56
81
|
|
82
|
+
Schema load from `db/clickhouse_schema.rb` file:
|
83
|
+
|
84
|
+
$ rake clickhouse:schema:load
|
85
|
+
|
86
|
+
We use schema for emulate development or tests environment on PostgreSQL adapter.
|
87
|
+
|
57
88
|
### Insert and select data
|
58
89
|
|
59
90
|
```ruby
|
60
91
|
Action.where(url: 'http://example.com', date: Date.current).where.not(name: nil).order(created_at: :desc).limit(10)
|
61
|
-
|
92
|
+
# Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions WHERE actions.date = '2017-11-29' AND actions.url = 'http://example.com' AND (actions.name IS NOT NULL) ORDER BY actions.created_at DESC LIMIT 10
|
93
|
+
#=> #<ActiveRecord::Relation [#<Action *** >]>
|
62
94
|
|
63
95
|
Action.create(url: 'http://example.com', date: Date.yesterday)
|
64
|
-
|
96
|
+
# Clickhouse Action Load (10.8ms) INSERT INTO actions (url, date) VALUES ('http://example.com', '2017-11-28')
|
97
|
+
#=> true
|
98
|
+
|
99
|
+
ActionView.maximum(:date)
|
100
|
+
# Clickhouse (10.3ms) SELECT maxMerge(actions.date) FROM actions
|
101
|
+
#=> 'Wed, 29 Nov 2017'
|
65
102
|
```
|
66
103
|
|
67
|
-
|
104
|
+
## Donations
|
105
|
+
|
106
|
+
Donations to this project are going directly to [PNixx](https://github.com/PNixx), the original author of this project:
|
107
|
+
|
108
|
+
* BTC address: `1Lx2gaJtzfF2dxGFxB65YtY5kNY9xUi6ia`
|
109
|
+
* ETH address: `0x6F094365A70fe7836A633d2eE80A1FA9758234d5`
|
110
|
+
* XMR address: `42gP71qLB5M43RuDnrQ3vSJFFxis9Kw9VMURhpx9NLQRRwNvaZRjm2TFojAMC8Fk1BQhZNKyWhoyJSn5Ak9kppgZPjE17Zh`
|
68
111
|
|
69
112
|
## Development
|
70
113
|
|
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
# coding: utf-8
|
2
3
|
|
3
4
|
lib = File.expand_path('../lib', __FILE__)
|
4
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require File.expand_path('../lib/clickhouse-activerecord/version', __FILE__)
|
5
7
|
|
6
8
|
Gem::Specification.new do |spec|
|
7
9
|
spec.name = 'clickhouse-activerecord'
|
8
|
-
spec.version =
|
10
|
+
spec.version = ClickhouseActiverecord::VERSION
|
9
11
|
spec.authors = ['Sergey Odintsov']
|
10
12
|
spec.email = ['nixx.dj@gmail.com']
|
11
13
|
|
@@ -22,8 +24,10 @@ Gem::Specification.new do |spec|
|
|
22
24
|
spec.require_paths = ['lib']
|
23
25
|
|
24
26
|
spec.add_dependency "bundler", ">= 1.13.4"
|
25
|
-
spec.add_dependency '
|
27
|
+
spec.add_dependency 'activerecord', '>= 5.2'
|
26
28
|
|
27
29
|
spec.add_development_dependency 'bundler', '~> 1.15'
|
28
30
|
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec'
|
32
|
+
spec.add_development_dependency 'pry'
|
29
33
|
end
|
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
module ConnectionAdapters
|
3
5
|
module Clickhouse
|
4
6
|
module OID # :nodoc:
|
5
7
|
class BigInteger < Type::BigInteger # :nodoc:
|
6
|
-
|
7
|
-
|
8
|
+
def type
|
9
|
+
:big_integer
|
10
|
+
end
|
8
11
|
|
9
12
|
def limit
|
10
13
|
DEFAULT_LIMIT
|
@@ -1,9 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
module ConnectionAdapters
|
3
5
|
module Clickhouse
|
4
6
|
module OID # :nodoc:
|
5
7
|
class DateTime < Type::DateTime # :nodoc:
|
6
8
|
|
9
|
+
def serialize(value)
|
10
|
+
value = super
|
11
|
+
return value unless value.acts_like?(:time)
|
12
|
+
|
13
|
+
value.to_time.strftime('%Y-%m-%d %H:%M:%S')
|
14
|
+
end
|
15
|
+
|
7
16
|
def type_cast_from_database(value)
|
8
17
|
value
|
9
18
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Clickhouse
|
6
|
+
class SchemaCreation < AbstractAdapter::SchemaCreation# :nodoc:
|
7
|
+
|
8
|
+
def visit_AddColumnDefinition(o)
|
9
|
+
+"ADD COLUMN #{accept(o.column)}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_column_options!(sql, options)
|
13
|
+
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
|
14
|
+
if options[:null] || options[:null].nil?
|
15
|
+
sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
|
16
|
+
end
|
17
|
+
sql.gsub!(/(\sString)\(\d+\)/, '\1')
|
18
|
+
sql
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_table_options!(create_sql, options)
|
22
|
+
if options[:options].present?
|
23
|
+
create_sql << " ENGINE = #{options[:options]}"
|
24
|
+
else
|
25
|
+
create_sql << " ENGINE = Log()"
|
26
|
+
end
|
27
|
+
|
28
|
+
create_sql
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Clickhouse
|
6
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
7
|
+
|
8
|
+
def integer(*args, **options)
|
9
|
+
if options[:limit] == 8
|
10
|
+
args.each { |name| column(name, :big_integer, options.except(:limit)) }
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Clickhouse
|
6
|
+
module SchemaStatements
|
7
|
+
def execute(sql, name = nil)
|
8
|
+
do_execute(sql, name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil)
|
12
|
+
new_sql = sql.dup.sub(/ (DEFAULT )?VALUES/, " VALUES")
|
13
|
+
do_execute(new_sql, name, format: nil)
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def exec_query(sql, name = nil, binds = [], prepare: false)
|
18
|
+
result = do_execute(sql, name)
|
19
|
+
ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
|
20
|
+
end
|
21
|
+
|
22
|
+
def exec_update(_sql, _name = nil, _binds = [])
|
23
|
+
raise ActiveRecord::ActiveRecordError, 'Clickhouse update is not supported'
|
24
|
+
end
|
25
|
+
|
26
|
+
def exec_delete(_sql, _name = nil, _binds = [])
|
27
|
+
raise ActiveRecord::ActiveRecordError, 'Clickhouse delete is not supported'
|
28
|
+
end
|
29
|
+
|
30
|
+
def tables(name = nil)
|
31
|
+
result = do_system_execute('SHOW TABLES', name)
|
32
|
+
return [] if result.nil?
|
33
|
+
result['data'].flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def table_options(table)
|
37
|
+
sql = do_system_execute("SHOW CREATE TABLE #{table}")['data'].try(:first).try(:first)
|
38
|
+
{ options: sql.gsub(/^(?:.*?)ENGINE = (.*?)$/, '\\1') }
|
39
|
+
end
|
40
|
+
|
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)
|
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
|
+
# Not indexes on clickhouse
|
73
|
+
def indexes(table_name, name = nil)
|
74
|
+
[]
|
75
|
+
end
|
76
|
+
|
77
|
+
def data_sources
|
78
|
+
tables
|
79
|
+
end
|
80
|
+
|
81
|
+
def do_system_execute(sql, name = nil)
|
82
|
+
log_with_debug(sql, "#{adapter_name} #{name}") do
|
83
|
+
res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
|
84
|
+
|
85
|
+
process_response(res)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
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')
|
96
|
+
log(sql, "#{adapter_name} #{name}") do
|
97
|
+
formatted_sql = apply_format(sql, format)
|
98
|
+
res = @connection.post("/?#{@config.to_param}", formatted_sql)
|
99
|
+
|
100
|
+
process_response(res)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_response(res)
|
105
|
+
case res.code.to_i
|
106
|
+
when 200
|
107
|
+
res.body.presence && JSON.parse(res.body)
|
108
|
+
else
|
109
|
+
raise ActiveRecord::ActiveRecordError,
|
110
|
+
"Response code: #{res.code}:\n#{res.body}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def log_with_debug(sql, name = nil)
|
115
|
+
return yield unless @debug
|
116
|
+
log(sql, "#{name} (system)") { yield }
|
117
|
+
end
|
118
|
+
|
119
|
+
def schema_creation
|
120
|
+
Clickhouse::SchemaCreation.new(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
def create_table_definition(*args)
|
124
|
+
Clickhouse::TableDefinition.new(*args)
|
125
|
+
end
|
126
|
+
|
127
|
+
def new_column_from_field(table_name, field)
|
128
|
+
sql_type = field[1]
|
129
|
+
type_metadata = fetch_type_metadata(sql_type)
|
130
|
+
default = field[3]
|
131
|
+
default_value = extract_value_from_default(default)
|
132
|
+
default_function = extract_default_function(default_value, default)
|
133
|
+
ClickhouseColumn.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), table_name, default_function)
|
134
|
+
end
|
135
|
+
|
136
|
+
protected
|
137
|
+
|
138
|
+
def table_structure(table_name)
|
139
|
+
result = do_system_execute("DESCRIBE TABLE #{table_name}", table_name)
|
140
|
+
data = result['data']
|
141
|
+
|
142
|
+
return data unless data.empty?
|
143
|
+
|
144
|
+
raise ActiveRecord::StatementInvalid,
|
145
|
+
"Could not find table '#{table_name}'"
|
146
|
+
end
|
147
|
+
alias column_definitions table_structure
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Extracts the value from a PostgreSQL column default definition.
|
152
|
+
def extract_value_from_default(default)
|
153
|
+
case default
|
154
|
+
# Quoted types
|
155
|
+
when /\Anow\(\)\z/m
|
156
|
+
nil
|
157
|
+
# Boolean types
|
158
|
+
when "true".freeze, "false".freeze
|
159
|
+
default
|
160
|
+
# Object identifier types
|
161
|
+
when /\A-?\d+\z/
|
162
|
+
$1
|
163
|
+
else
|
164
|
+
# Anything else is blank, some user type, or some function
|
165
|
+
# and we can't know the value of that, so return nil.
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def extract_default_function(default_value, default) # :nodoc:
|
171
|
+
default if has_default_function?(default_value, default)
|
172
|
+
end
|
173
|
+
|
174
|
+
def has_default_function?(default_value, default) # :nodoc:
|
175
|
+
!default_value && (%r{\w+\(.*\)} === default)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -1,7 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clickhouse-activerecord/arel/visitors/to_sql'
|
1
4
|
require 'active_record/connection_adapters/abstract_adapter'
|
2
5
|
require 'active_record/connection_adapters/clickhouse/oid/date'
|
3
6
|
require 'active_record/connection_adapters/clickhouse/oid/date_time'
|
4
7
|
require 'active_record/connection_adapters/clickhouse/oid/big_integer'
|
8
|
+
require 'active_record/connection_adapters/clickhouse/schema_definitions'
|
9
|
+
require 'active_record/connection_adapters/clickhouse/schema_creation'
|
10
|
+
require 'active_record/connection_adapters/clickhouse/schema_statements'
|
11
|
+
require 'net/http'
|
5
12
|
|
6
13
|
module ActiveRecord
|
7
14
|
class Base
|
@@ -9,7 +16,7 @@ module ActiveRecord
|
|
9
16
|
# Establishes a connection to the database that's used by all Active Record objects
|
10
17
|
def clickhouse_connection(config)
|
11
18
|
config = config.symbolize_keys
|
12
|
-
host = config[:host]
|
19
|
+
host = config[:host] || 'localhost'
|
13
20
|
port = config[:port] || 8123
|
14
21
|
|
15
22
|
if config.key?(:database)
|
@@ -18,51 +25,25 @@ module ActiveRecord
|
|
18
25
|
raise ArgumentError, 'No database specified. Missing argument: database.'
|
19
26
|
end
|
20
27
|
|
21
|
-
ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact)
|
28
|
+
ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact, config[:debug])
|
22
29
|
end
|
23
30
|
end
|
24
31
|
end
|
25
32
|
|
26
|
-
module
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
# Extracts the value from a PostgreSQL column default definition.
|
33
|
-
def extract_value_from_default(default)
|
34
|
-
case default
|
35
|
-
# Quoted types
|
36
|
-
when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
|
37
|
-
# The default 'now'::date is CURRENT_DATE
|
38
|
-
if $1 == "now".freeze && $2 == "date".freeze
|
39
|
-
nil
|
40
|
-
else
|
41
|
-
$1.gsub("''".freeze, "'".freeze)
|
42
|
-
end
|
43
|
-
# Boolean types
|
44
|
-
when "true".freeze, "false".freeze
|
45
|
-
default
|
46
|
-
# Numeric types
|
47
|
-
when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
|
48
|
-
$1
|
49
|
-
# Object identifier types
|
50
|
-
when /\A-?\d+\z/
|
51
|
-
$1
|
52
|
-
else
|
53
|
-
# Anything else is blank, some user type, or some function
|
54
|
-
# and we can't know the value of that, so return nil.
|
55
|
-
nil
|
56
|
-
end
|
33
|
+
module ModelSchema
|
34
|
+
module ClassMethods
|
35
|
+
def is_view
|
36
|
+
@is_view || false
|
57
37
|
end
|
58
|
-
|
59
|
-
def
|
60
|
-
|
38
|
+
# @param [Boolean] value
|
39
|
+
def is_view=(value)
|
40
|
+
@is_view = value
|
61
41
|
end
|
42
|
+
end
|
43
|
+
end
|
62
44
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
45
|
+
module ConnectionAdapters
|
46
|
+
class ClickhouseColumn < Column
|
66
47
|
|
67
48
|
end
|
68
49
|
|
@@ -70,26 +51,34 @@ module ActiveRecord
|
|
70
51
|
ADAPTER_NAME = 'Clickhouse'.freeze
|
71
52
|
|
72
53
|
NATIVE_DATABASE_TYPES = {
|
73
|
-
string: { name: 'String'
|
54
|
+
string: { name: 'String' },
|
74
55
|
integer: { name: 'UInt32' },
|
75
|
-
big_integer: { name: 'UInt64'
|
56
|
+
big_integer: { name: 'UInt64' },
|
76
57
|
float: { name: 'Float32' },
|
58
|
+
decimal: { name: 'Decimal' },
|
77
59
|
datetime: { name: 'DateTime' },
|
78
60
|
date: { name: 'Date' },
|
79
61
|
boolean: { name: 'UInt8' }
|
80
62
|
}.freeze
|
81
63
|
|
64
|
+
include Clickhouse::SchemaStatements
|
65
|
+
|
82
66
|
# Initializes and connects a Clickhouse adapter.
|
83
|
-
def initialize(connection, logger, connection_parameters, config)
|
67
|
+
def initialize(connection, logger, connection_parameters, config, debug = false)
|
84
68
|
super(connection, logger)
|
85
69
|
@connection_parameters = connection_parameters
|
86
70
|
@config = config
|
71
|
+
@debug = debug
|
87
72
|
|
88
73
|
@prepared_statements = false
|
89
74
|
|
90
75
|
connect
|
91
76
|
end
|
92
77
|
|
78
|
+
def arel_visitor # :nodoc:
|
79
|
+
ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
|
80
|
+
end
|
81
|
+
|
93
82
|
def native_database_types #:nodoc:
|
94
83
|
NATIVE_DATABASE_TYPES
|
95
84
|
end
|
@@ -100,11 +89,13 @@ module ActiveRecord
|
|
100
89
|
|
101
90
|
def extract_limit(sql_type) # :nodoc:
|
102
91
|
case sql_type
|
103
|
-
when
|
104
|
-
|
105
|
-
when /Nullable
|
106
|
-
|
107
|
-
when /Nullable
|
92
|
+
when /(Nullable)?\(?String\)?/
|
93
|
+
super('String')
|
94
|
+
when /(Nullable)?\(?U?Int8\)?/
|
95
|
+
super('int2')
|
96
|
+
when /(Nullable)?\(?U?Int(16|32)\)?/
|
97
|
+
super('int4')
|
98
|
+
when /(Nullable)?\(?U?Int(64)\)?/
|
108
99
|
8
|
109
100
|
else
|
110
101
|
super
|
@@ -113,93 +104,59 @@ module ActiveRecord
|
|
113
104
|
|
114
105
|
def initialize_type_map(m) # :nodoc:
|
115
106
|
super
|
116
|
-
register_class_with_limit m,
|
117
|
-
register_class_with_limit m, 'Nullable(String)', Type::String
|
118
|
-
register_class_with_limit m, 'Uint8', Type::UnsignedInteger
|
107
|
+
register_class_with_limit m, %r(String), Type::String
|
119
108
|
register_class_with_limit m, 'Date', Clickhouse::OID::Date
|
120
109
|
register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
|
121
|
-
m
|
122
|
-
m.alias_type '
|
123
|
-
m.
|
124
|
-
m
|
125
|
-
m
|
126
|
-
m.alias_type '
|
127
|
-
m.alias_type '
|
128
|
-
|
129
|
-
|
130
|
-
# Queries the database and returns the results in an Array-like object
|
131
|
-
def query(sql, _name = nil)
|
132
|
-
res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
|
133
|
-
raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}" unless res.code.to_i == 200
|
134
|
-
JSON.parse res.body
|
135
|
-
end
|
136
|
-
|
137
|
-
def execute(sql, name = nil)
|
138
|
-
log(sql, "#{adapter_name} #{name}") do
|
139
|
-
query sql, name
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def exec_query(sql, name = nil, binds = [], prepare: false)
|
144
|
-
result = execute(sql, name)
|
145
|
-
ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
|
110
|
+
register_class_with_limit m, %r(Uint8), Type::UnsignedInteger
|
111
|
+
m.alias_type 'UInt16', 'UInt8'
|
112
|
+
m.alias_type 'UInt32', 'UInt8'
|
113
|
+
register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
|
114
|
+
register_class_with_limit m, %r(Int8), Type::Integer
|
115
|
+
m.alias_type 'Int16', 'Int8'
|
116
|
+
m.alias_type 'Int32', 'Int8'
|
117
|
+
register_class_with_limit m, %r(Int64), Type::Integer
|
146
118
|
end
|
147
119
|
|
148
120
|
# Executes insert +sql+ statement in the context of this connection using
|
149
121
|
# +binds+ as the bind substitutes. +name+ is logged along with
|
150
122
|
# the executed +sql+ statement.
|
151
|
-
def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil)
|
152
|
-
log(sql, "#{adapter_name} #{name}") do
|
153
|
-
res = @connection.post("/?#{@config.to_param}", sql)
|
154
|
-
raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}" unless res.code.to_i == 200
|
155
|
-
true
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def update(_arel, _name = nil, _binds = [])
|
160
|
-
raise ActiveRecord::ActiveRecordError, 'Clickhouse update is not supported'
|
161
|
-
end
|
162
|
-
|
163
|
-
def delete(_arel, _name = nil, _binds = [])
|
164
|
-
raise ActiveRecord::ActiveRecordError, 'Clickhouse delete is not supported'
|
165
|
-
end
|
166
123
|
|
167
124
|
# SCHEMA STATEMENTS ========================================
|
168
125
|
|
169
|
-
def
|
170
|
-
|
126
|
+
def primary_key(table_name) #:nodoc:
|
127
|
+
pk = table_structure(table_name).first
|
128
|
+
return 'id' if pk.present? && pk[0] == 'id'
|
129
|
+
false
|
171
130
|
end
|
172
131
|
|
173
|
-
def
|
174
|
-
|
175
|
-
type_metadata = fetch_type_metadata(sql_type)
|
176
|
-
ClickhouseColumn.new(field[0], field[3].present? ? field[3] : nil, type_metadata, field[1].include?('Nullable'), table_name, nil)
|
132
|
+
def create_schema_dumper(options) # :nodoc:
|
133
|
+
ClickhouseActiverecord::SchemaDumper.create(self, options)
|
177
134
|
end
|
178
135
|
|
179
|
-
|
180
|
-
|
181
|
-
|
136
|
+
# Create a new ClickHouse database.
|
137
|
+
def create_database(name)
|
138
|
+
sql = "CREATE DATABASE #{quote_table_name(name)}"
|
139
|
+
log_with_debug(sql, adapter_name) do
|
140
|
+
res = @connection.post("/?#{@config.except(:database).to_param}", "CREATE DATABASE #{quote_table_name(name)}")
|
141
|
+
process_response(res)
|
142
|
+
end
|
182
143
|
end
|
183
144
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
145
|
+
# Drops a ClickHouse database.
|
146
|
+
def drop_database(name) #:nodoc:
|
147
|
+
sql = "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
|
148
|
+
log_with_debug(sql, adapter_name) do
|
149
|
+
res = @connection.post("/?#{@config.except(:database).to_param}", sql)
|
150
|
+
process_response(res)
|
151
|
+
end
|
188
152
|
end
|
189
153
|
|
190
|
-
def
|
191
|
-
|
154
|
+
def drop_table(table_name, options = {}) # :nodoc:
|
155
|
+
do_execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
|
192
156
|
end
|
193
157
|
|
194
158
|
protected
|
195
159
|
|
196
|
-
def table_structure(table_name)
|
197
|
-
data = query("DESCRIBE TABLE #{table_name}", table_name)['data']
|
198
|
-
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if data.empty?
|
199
|
-
data
|
200
|
-
end
|
201
|
-
alias column_definitions table_structure
|
202
|
-
|
203
160
|
def last_inserted_id(result)
|
204
161
|
result
|
205
162
|
end
|
@@ -1,6 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/connection_adapters/clickhouse_adapter'
|
2
4
|
|
3
|
-
|
5
|
+
if defined?(Rails::Railtie)
|
6
|
+
require 'clickhouse-activerecord/railtie'
|
7
|
+
require 'clickhouse-activerecord/schema_dumper'
|
8
|
+
require 'clickhouse-activerecord/tasks'
|
9
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/clickhouse/, "ClickhouseActiverecord::Tasks")
|
10
|
+
end
|
4
11
|
|
5
12
|
module ClickhouseActiverecord
|
6
13
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'arel/visitors/to_sql'
|
2
|
+
|
3
|
+
module ClickhouseActiverecord
|
4
|
+
module Arel
|
5
|
+
module Visitors
|
6
|
+
class ToSql < ::Arel::Visitors::ToSql
|
7
|
+
|
8
|
+
def aggregate(name, o, collector)
|
9
|
+
# todo how get model class from request? This method works with only rails 4.2.
|
10
|
+
# replacing function name for materialized view
|
11
|
+
# if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation && o.expressions.first.relation.engine && o.expressions.first.relation.engine.is_view
|
12
|
+
# super("#{name.downcase}Merge", o, collector)
|
13
|
+
# else
|
14
|
+
super
|
15
|
+
# end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ClickhouseActiverecord
|
2
|
+
class SchemaDumper < ::ActiveRecord::ConnectionAdapters::SchemaDumper
|
3
|
+
|
4
|
+
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)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClickhouseActiverecord
|
4
|
+
class Tasks
|
5
|
+
|
6
|
+
delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base
|
7
|
+
|
8
|
+
def initialize(configuration)
|
9
|
+
@configuration = configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
establish_master_connection
|
14
|
+
connection.create_database @configuration["database"]
|
15
|
+
rescue ActiveRecord::StatementInvalid => e
|
16
|
+
if e.cause.to_s.include?('already exists')
|
17
|
+
raise ActiveRecord::Tasks::DatabaseAlreadyExists
|
18
|
+
else
|
19
|
+
raise
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def drop
|
24
|
+
establish_master_connection
|
25
|
+
connection.drop_database @configuration["database"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def purge
|
29
|
+
clear_active_connections!
|
30
|
+
drop
|
31
|
+
create
|
32
|
+
end
|
33
|
+
|
34
|
+
def migrate
|
35
|
+
check_target_version
|
36
|
+
|
37
|
+
verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
|
38
|
+
scope = ENV["SCOPE"]
|
39
|
+
verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, verbose
|
40
|
+
binding.pry
|
41
|
+
connection.migration_context.migrate(target_version) do |migration|
|
42
|
+
scope.blank? || scope == migration.scope
|
43
|
+
end
|
44
|
+
ActiveRecord::Base.clear_cache!
|
45
|
+
ensure
|
46
|
+
ActiveRecord::Migration.verbose = verbose_was
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def establish_master_connection
|
52
|
+
establish_connection @configuration
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_target_version
|
56
|
+
if target_version && !(ActiveRecord::Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
|
57
|
+
raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def target_version
|
62
|
+
ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rails/generators/active_record/migration/migration_generator'
|
2
|
+
|
3
|
+
class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
|
4
|
+
source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates")
|
5
|
+
|
6
|
+
def create_migration_file
|
7
|
+
set_local_assigns!
|
8
|
+
validate_file_name!
|
9
|
+
migration_template @migration_template, "db/migrate_clickhouse/#{file_name}.rb"
|
10
|
+
end
|
11
|
+
end
|
data/lib/tasks/clickhouse.rake
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :clickhouse do
|
2
4
|
|
5
|
+
task load_config: :environment do
|
6
|
+
ENV['SCHEMA'] = "db/clickhouse_schema.rb"
|
7
|
+
ActiveRecord::Migrator.migrations_paths = ["db/migrate_clickhouse"]
|
8
|
+
ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
|
9
|
+
end
|
10
|
+
|
3
11
|
namespace :schema do
|
4
12
|
|
5
13
|
# todo not testing
|
6
14
|
desc 'Load database schema'
|
7
|
-
task load: :
|
8
|
-
ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
|
15
|
+
task load: :load_config do
|
9
16
|
load("#{Rails.root}/db/clickhouse_schema.rb")
|
10
17
|
end
|
11
18
|
|
@@ -14,10 +21,35 @@ namespace :clickhouse do
|
|
14
21
|
filename = "#{Rails.root}/db/clickhouse_schema.rb"
|
15
22
|
File.open(filename, 'w:utf-8') do |file|
|
16
23
|
ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
|
17
|
-
|
24
|
+
ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
28
|
end
|
22
29
|
|
30
|
+
desc 'Creates the database from DATABASE_URL or config/database.yml'
|
31
|
+
task create: [:load_config] do
|
32
|
+
ActiveRecord::Tasks::DatabaseTasks.create(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Drops the database from DATABASE_URL or config/database.yml'
|
36
|
+
task drop: [:load_config, 'db:check_protected_environments'] do
|
37
|
+
ActiveRecord::Tasks::DatabaseTasks.drop(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'Empty the database from DATABASE_URL or config/database.yml'
|
41
|
+
task purge: [:load_config, 'db:check_protected_environments'] do
|
42
|
+
ActiveRecord::Tasks::DatabaseTasks.purge(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
|
43
|
+
end
|
44
|
+
|
45
|
+
# desc 'Resets your database using your migrations for the current environment'
|
46
|
+
task reset: :load_config do
|
47
|
+
Rake::Task['clickhouse:purge'].execute
|
48
|
+
Rake::Task['clickhouse:migrate'].execute
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'Migrate the clickhouse database'
|
52
|
+
task migrate: :load_config do
|
53
|
+
Rake::Task['db:migrate'].execute
|
54
|
+
end
|
23
55
|
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.3.1
|
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: 2019-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,19 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.13.4
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '5.
|
33
|
+
version: '5.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '5.
|
40
|
+
version: '5.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
description: ActiveRecord adapter for ClickHouse
|
70
98
|
email:
|
71
99
|
- nixx.dj@gmail.com
|
@@ -74,6 +102,8 @@ extensions: []
|
|
74
102
|
extra_rdoc_files: []
|
75
103
|
files:
|
76
104
|
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- CHANGELOG.md
|
77
107
|
- CODE_OF_CONDUCT.md
|
78
108
|
- Gemfile
|
79
109
|
- LICENSE.txt
|
@@ -85,9 +115,17 @@ files:
|
|
85
115
|
- lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
|
86
116
|
- lib/active_record/connection_adapters/clickhouse/oid/date.rb
|
87
117
|
- lib/active_record/connection_adapters/clickhouse/oid/date_time.rb
|
118
|
+
- lib/active_record/connection_adapters/clickhouse/schema_creation.rb
|
119
|
+
- lib/active_record/connection_adapters/clickhouse/schema_definitions.rb
|
120
|
+
- lib/active_record/connection_adapters/clickhouse/schema_statements.rb
|
88
121
|
- lib/active_record/connection_adapters/clickhouse_adapter.rb
|
89
122
|
- lib/clickhouse-activerecord.rb
|
90
|
-
- lib/clickhouse/
|
123
|
+
- lib/clickhouse-activerecord/arel/visitors/to_sql.rb
|
124
|
+
- lib/clickhouse-activerecord/railtie.rb
|
125
|
+
- lib/clickhouse-activerecord/schema_dumper.rb
|
126
|
+
- lib/clickhouse-activerecord/tasks.rb
|
127
|
+
- lib/clickhouse-activerecord/version.rb
|
128
|
+
- lib/generators/clickhouse_migration_generator.rb
|
91
129
|
- lib/tasks/clickhouse.rake
|
92
130
|
homepage: https://github.com/pnixx/clickhouse-activerecord
|
93
131
|
licenses:
|
@@ -108,10 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
146
|
- !ruby/object:Gem::Version
|
109
147
|
version: '0'
|
110
148
|
requirements: []
|
111
|
-
|
112
|
-
rubygems_version: 2.6.12
|
149
|
+
rubygems_version: 3.0.1
|
113
150
|
signing_key:
|
114
151
|
specification_version: 4
|
115
152
|
summary: ClickHouse ActiveRecord
|
116
153
|
test_files: []
|
117
|
-
has_rdoc:
|