clickhouse-activerecord 0.2.2 → 0.3.1
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/.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:
|