clickhouse-activerecord 0.4.7 → 0.5.7
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +55 -1
- data/core_extensions/active_record/migration/command_recorder.rb +27 -0
- data/lib/active_record/connection_adapters/clickhouse/oid/array.rb +30 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +56 -6
- data/lib/active_record/connection_adapters/clickhouse/schema_definitions.rb +26 -6
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +17 -8
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +133 -26
- data/lib/clickhouse-activerecord/migration.rb +48 -16
- data/lib/clickhouse-activerecord/schema_dumper.rb +32 -3
- data/lib/clickhouse-activerecord/tasks.rb +10 -2
- data/lib/clickhouse-activerecord/version.rb +1 -1
- data/lib/clickhouse-activerecord.rb +3 -0
- data/lib/generators/clickhouse_migration_generator.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79b43575e5eef93daa11e1a2aca2984f696cbef59021f55d1470b2dc0a5d1e3d
|
4
|
+
data.tar.gz: b79cccb85d8a07fa4dc533d7b7e9042eeaa2afead26214c075b3131e1843394b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 840b01cc1d5b88eee5e031fada23cfd35f7a8d948a35a767b2f5baf1596af665f28c56778e699960511004367b16ee3130be1f5bd2ee713a9a0d5053a9b58a37
|
7
|
+
data.tar.gz: 4e613a19c51b362a05634da06d9528809ed09f01ebec4eac9984656156e9b877bdc4ce321e24bea63ae4e49d85609fa9dda781db60159e60b6d6ee06e8a99a3e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
### Version 0.5.6 (Oct 25, 2021)
|
2
|
+
|
3
|
+
* Added auto creating service distributed tables and additional options for creating view [@ygreeek](https://github.com/ygreeek)
|
4
|
+
* Added default user agent
|
5
|
+
|
6
|
+
### Version 0.5.3 (Sep 22, 2021)
|
7
|
+
|
8
|
+
* Fix replica cluster for a new syntax MergeTree
|
9
|
+
* Fix support rails 5.2 on alter table
|
10
|
+
* Support array type of column
|
11
|
+
* Support Rails 6.1.0 [@bdevel](https://github.com/bdevel)
|
12
|
+
|
13
|
+
### Version 0.4.10 (Mar 10, 2021)
|
14
|
+
|
15
|
+
* Support ClickHouse 20.9+
|
16
|
+
* Fix schema create / dump
|
17
|
+
* Support all integer types through :limit and :unsigned [@bdevel](https://github.com/bdevel)
|
18
|
+
|
1
19
|
### Version 0.4.4 (Sep 23, 2020)
|
2
20
|
|
3
21
|
* Full support migration and rollback database
|
data/README.md
CHANGED
@@ -1,7 +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
|
4
|
+
Support ClickHouse version from 20.9 LTS.
|
5
5
|
|
6
6
|
## Installation
|
7
7
|
|
@@ -35,6 +35,21 @@ default: &default
|
|
35
35
|
replica_name: '{replica}' # replica macros name, optional for creating replicated tables
|
36
36
|
```
|
37
37
|
|
38
|
+
Alternatively if you wish to pass a custom `Net::HTTP` transport (or any other
|
39
|
+
object which supports a `.post()` function with the same parameters as
|
40
|
+
`Net::HTTP`'s), you can do this directly instead of specifying
|
41
|
+
`host`/`port`/`ssl`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class ActionView < ActiveRecord::Base
|
45
|
+
establish_connection(
|
46
|
+
adapter: 'clickhouse',
|
47
|
+
database: 'database',
|
48
|
+
connection: Net::HTTP.start('http://example.org', 8123)
|
49
|
+
)
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
38
53
|
## Usage in Rails 5
|
39
54
|
|
40
55
|
Add your `database.yml` connection information with postfix `_clickhouse` for you environment:
|
@@ -162,6 +177,45 @@ ActionView.maximum(:date)
|
|
162
177
|
#=> 'Wed, 29 Nov 2017'
|
163
178
|
```
|
164
179
|
|
180
|
+
|
181
|
+
### Migration Data Types
|
182
|
+
|
183
|
+
Integer types are unsigned by default. Specify signed values with `:unsigned =>
|
184
|
+
false`. The default integer is `UInt32`
|
185
|
+
|
186
|
+
| Type (bit size) | Range | :limit (byte size) |
|
187
|
+
| :--- | :----: | ---: |
|
188
|
+
| Int8 | -128 to 127 | 1 |
|
189
|
+
| Int16 | -32768 to 32767 | 2 |
|
190
|
+
| Int32 | -2147483648 to 2,147,483,647 | 3,4 |
|
191
|
+
| Int64 | -9223372036854775808 to 9223372036854775807] | 5,6,7,8 |
|
192
|
+
| Int128 | ... | 9 - 15 |
|
193
|
+
| Int256 | ... | 16+ |
|
194
|
+
| UInt8 | 0 to 255 | 1 |
|
195
|
+
| UInt16 | 0 to 65,535 | 2 |
|
196
|
+
| UInt32 | 0 to 4,294,967,295 | 3,4 |
|
197
|
+
| UInt64 | 0 to 18446744073709551615 | 5,6,7,8 |
|
198
|
+
| UInt256 | 0 to ... | 8+ |
|
199
|
+
| Array | ... | ... |
|
200
|
+
|
201
|
+
Example:
|
202
|
+
|
203
|
+
``` ruby
|
204
|
+
class CreateDataItems < ActiveRecord::Migration
|
205
|
+
def change
|
206
|
+
create_table "data_items", id: false, options: "VersionedCollapsingMergeTree(sign, version) PARTITION BY toYYYYMM(day) ORDER BY category", force: :cascade do |t|
|
207
|
+
t.date "day", null: false
|
208
|
+
t.string "category", null: false
|
209
|
+
t.integer "value_in", null: false
|
210
|
+
t.integer "sign", limit: 1, unsigned: false, default: -> { "CAST(1, 'Int8')" }, null: false
|
211
|
+
t.integer "version", limit: 8, default: -> { "CAST(toUnixTimestamp(now()), 'UInt64')" }, null: false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
```
|
217
|
+
|
218
|
+
|
165
219
|
### Using replica and cluster params in connection parameters
|
166
220
|
|
167
221
|
```yml
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module CoreExtensions
|
2
|
+
module ActiveRecord
|
3
|
+
module Migration
|
4
|
+
module CommandRecorder
|
5
|
+
def create_table_with_distributed(*args, &block)
|
6
|
+
record(:create_table_with_distributed, args, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_view(*args, &block)
|
10
|
+
record(:create_view, args, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def invert_create_table_with_distributed(args)
|
16
|
+
table_name, options = args
|
17
|
+
[:drop_table_with_distributed, table_name, options]
|
18
|
+
end
|
19
|
+
|
20
|
+
def invert_create_view(args)
|
21
|
+
view_name, options = args
|
22
|
+
[:drop_table, view_name, options]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Clickhouse
|
6
|
+
module OID # :nodoc:
|
7
|
+
class Array < Type::Value # :nodoc:
|
8
|
+
|
9
|
+
def initialize(sql_type)
|
10
|
+
@subtype = case sql_type
|
11
|
+
when /U?Int\d+/
|
12
|
+
:integer
|
13
|
+
when /DateTime/
|
14
|
+
:datetime
|
15
|
+
when /Date/
|
16
|
+
:date
|
17
|
+
else
|
18
|
+
:string
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
@subtype
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,9 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
begin
|
3
|
+
require "active_record/connection_adapters/deduplicable"
|
4
|
+
rescue LoadError => e
|
5
|
+
# Rails < 6.1 does not have this file in this location, ignore
|
6
|
+
end
|
7
|
+
|
8
|
+
require "active_record/connection_adapters/abstract/schema_creation"
|
2
9
|
|
3
10
|
module ActiveRecord
|
4
11
|
module ConnectionAdapters
|
5
12
|
module Clickhouse
|
6
|
-
class SchemaCreation <
|
13
|
+
class SchemaCreation < ConnectionAdapters::SchemaCreation# :nodoc:
|
7
14
|
|
8
15
|
def visit_AddColumnDefinition(o)
|
9
16
|
sql = +"ADD COLUMN #{accept(o.column)}"
|
@@ -15,14 +22,23 @@ module ActiveRecord
|
|
15
22
|
if options[:null] || options[:null].nil?
|
16
23
|
sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
|
17
24
|
end
|
25
|
+
if options[:array]
|
26
|
+
sql.gsub!(/\s+(.*)/, ' Array(\1)')
|
27
|
+
end
|
18
28
|
sql.gsub!(/(\sString)\(\d+\)/, '\1')
|
19
29
|
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
|
20
30
|
sql
|
21
31
|
end
|
22
32
|
|
23
33
|
def add_table_options!(create_sql, options)
|
24
|
-
|
25
|
-
|
34
|
+
opts = options[:options]
|
35
|
+
if options.respond_to?(:options)
|
36
|
+
# rails 6.1
|
37
|
+
opts ||= options.options
|
38
|
+
end
|
39
|
+
|
40
|
+
if opts.present?
|
41
|
+
create_sql << " ENGINE = #{opts}"
|
26
42
|
else
|
27
43
|
create_sql << " ENGINE = Log()"
|
28
44
|
end
|
@@ -30,17 +46,48 @@ module ActiveRecord
|
|
30
46
|
create_sql
|
31
47
|
end
|
32
48
|
|
49
|
+
def add_as_clause!(create_sql, options)
|
50
|
+
return unless options.as
|
51
|
+
|
52
|
+
assign_database_to_subquery!(options.as) if options.view
|
53
|
+
create_sql << " AS #{to_sql(options.as)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def assign_database_to_subquery!(subquery)
|
57
|
+
# If you do not specify a database explicitly, ClickHouse will use the "default" database.
|
58
|
+
return unless subquery
|
59
|
+
|
60
|
+
match = subquery.match(/(?<=from)[^.\w]+(?<database>\w+(?=\.))?(?<table_name>[.\w]+)/i)
|
61
|
+
return unless match
|
62
|
+
return if match[:database]
|
63
|
+
|
64
|
+
subquery[match.begin(:table_name)...match.end(:table_name)] =
|
65
|
+
"#{current_database}.#{match[:table_name].sub('.', '')}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_to_clause!(create_sql, options)
|
69
|
+
# If you do not specify a database explicitly, ClickHouse will use the "default" database.
|
70
|
+
return unless options.to
|
71
|
+
|
72
|
+
match = options.to.match(/(?<database>.+(?=\.))?(?<table_name>.+)/i)
|
73
|
+
return unless match
|
74
|
+
return if match[:database]
|
75
|
+
|
76
|
+
create_sql << "TO #{current_database}.#{options.to.sub('.', '')} "
|
77
|
+
end
|
78
|
+
|
33
79
|
def visit_TableDefinition(o)
|
34
80
|
create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} "
|
35
81
|
create_sql << "IF NOT EXISTS " if o.if_not_exists
|
36
82
|
create_sql << "#{quote_table_name(o.name)} "
|
83
|
+
add_to_clause!(create_sql, o) if o.materialized
|
37
84
|
|
38
85
|
statements = o.columns.map { |c| accept c }
|
39
86
|
statements << accept(o.primary_keys) if o.primary_keys
|
40
|
-
|
41
87
|
create_sql << "(#{statements.join(', ')})" if statements.present?
|
42
|
-
|
43
|
-
create_sql
|
88
|
+
# Attach options for only table or materialized view without TO section
|
89
|
+
add_table_options!(create_sql, o) if !o.view || o.view && o.materialized && !o.to
|
90
|
+
add_as_clause!(create_sql, o)
|
44
91
|
create_sql
|
45
92
|
end
|
46
93
|
|
@@ -68,6 +115,9 @@ module ActiveRecord
|
|
68
115
|
change_column_sql
|
69
116
|
end
|
70
117
|
|
118
|
+
def current_database
|
119
|
+
ActiveRecord::Base.connection_db_config.database
|
120
|
+
end
|
71
121
|
end
|
72
122
|
end
|
73
123
|
end
|
@@ -5,7 +5,7 @@ module ActiveRecord
|
|
5
5
|
module Clickhouse
|
6
6
|
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
7
7
|
|
8
|
-
attr_reader :view, :materialized, :if_not_exists
|
8
|
+
attr_reader :view, :materialized, :if_not_exists, :to
|
9
9
|
|
10
10
|
def initialize(
|
11
11
|
conn,
|
@@ -17,6 +17,7 @@ module ActiveRecord
|
|
17
17
|
comment: nil,
|
18
18
|
view: false,
|
19
19
|
materialized: false,
|
20
|
+
to: nil,
|
20
21
|
**
|
21
22
|
)
|
22
23
|
@conn = conn
|
@@ -32,16 +33,35 @@ module ActiveRecord
|
|
32
33
|
@comment = comment
|
33
34
|
@view = view || materialized
|
34
35
|
@materialized = materialized
|
36
|
+
@to = to
|
35
37
|
end
|
36
38
|
|
37
39
|
def integer(*args, **options)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
# default to unsigned
|
41
|
+
unsigned = options[:unsigned]
|
42
|
+
unsigned = true if unsigned.nil?
|
43
|
+
|
44
|
+
kind = :uint32 # default
|
45
|
+
|
46
|
+
if options[:limit]
|
47
|
+
if unsigned
|
48
|
+
kind = :uint8 if options[:limit] == 1
|
49
|
+
kind = :uint16 if options[:limit] == 2
|
50
|
+
kind = :uint32 if [3,4].include?(options[:limit])
|
51
|
+
kind = :uint64 if [5,6,7].include?(options[:limit])
|
52
|
+
kind = :big_integer if options[:limit] == 8
|
53
|
+
kind = :uint256 if options[:limit] > 8
|
54
|
+
else
|
55
|
+
kind = :int8 if options[:limit] == 1
|
56
|
+
kind = :int16 if options[:limit] == 2
|
57
|
+
kind = :int32 if [3,4].include?(options[:limit])
|
58
|
+
kind = :int64 if options[:limit] > 5 && options[:limit] <= 8
|
59
|
+
kind = :int128 if options[:limit] > 8 && options[:limit] <= 16
|
60
|
+
kind = :int256 if options[:limit] > 16
|
61
|
+
end
|
42
62
|
end
|
63
|
+
args.each { |name| column(name, kind, **options.except(:limit, :unsigned)) }
|
43
64
|
end
|
44
|
-
|
45
65
|
end
|
46
66
|
end
|
47
67
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'clickhouse-activerecord/version'
|
4
|
+
|
3
5
|
module ActiveRecord
|
4
6
|
module ConnectionAdapters
|
5
7
|
module Clickhouse
|
@@ -17,8 +19,15 @@ module ActiveRecord
|
|
17
19
|
def exec_query(sql, name = nil, binds = [], prepare: false)
|
18
20
|
result = do_execute(sql, name)
|
19
21
|
ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
|
20
|
-
rescue
|
21
|
-
raise
|
22
|
+
rescue ActiveRecord::ActiveRecordError => e
|
23
|
+
raise e
|
24
|
+
rescue StandardError => e
|
25
|
+
raise ActiveRecord::ActiveRecordError, "Response: #{e.message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def exec_insert_all(sql, name)
|
29
|
+
do_execute(sql, name, format: nil)
|
30
|
+
true
|
22
31
|
end
|
23
32
|
|
24
33
|
def exec_update(_sql, _name = nil, _binds = [])
|
@@ -36,8 +45,8 @@ module ActiveRecord
|
|
36
45
|
end
|
37
46
|
|
38
47
|
def table_options(table)
|
39
|
-
sql =
|
40
|
-
{ options: sql.gsub(/^(?:.*?)ENGINE = (.*?)
|
48
|
+
sql = show_create_table(table)
|
49
|
+
{ options: sql.gsub(/^(?:.*?)(?:ENGINE = (.*?))?( AS SELECT .*?)?$/, '\\1').presence, as: sql.match(/^CREATE (?:.*?) AS (SELECT .*?)$/).try(:[], 1) }.compact
|
41
50
|
end
|
42
51
|
|
43
52
|
# Not indexes on clickhouse
|
@@ -51,7 +60,7 @@ module ActiveRecord
|
|
51
60
|
|
52
61
|
def do_system_execute(sql, name = nil)
|
53
62
|
log_with_debug(sql, "#{adapter_name} #{name}") do
|
54
|
-
res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
|
63
|
+
res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact", 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
|
55
64
|
|
56
65
|
process_response(res)
|
57
66
|
end
|
@@ -61,7 +70,7 @@ module ActiveRecord
|
|
61
70
|
log(sql, "#{adapter_name} #{name}") do
|
62
71
|
formatted_sql = apply_format(sql, format)
|
63
72
|
request_params = @config || {}
|
64
|
-
res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql)
|
73
|
+
res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
|
65
74
|
|
66
75
|
process_response(res)
|
67
76
|
end
|
@@ -114,8 +123,8 @@ module ActiveRecord
|
|
114
123
|
Clickhouse::SchemaCreation.new(self)
|
115
124
|
end
|
116
125
|
|
117
|
-
def create_table_definition(
|
118
|
-
Clickhouse::TableDefinition.new(self,
|
126
|
+
def create_table_definition(table_name, **options)
|
127
|
+
Clickhouse::TableDefinition.new(self, table_name, **options)
|
119
128
|
end
|
120
129
|
|
121
130
|
def new_column_from_field(table_name, field)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'clickhouse-activerecord/arel/visitors/to_sql'
|
4
4
|
require 'clickhouse-activerecord/arel/table'
|
5
5
|
require 'clickhouse-activerecord/migration'
|
6
|
+
require 'active_record/connection_adapters/clickhouse/oid/array'
|
6
7
|
require 'active_record/connection_adapters/clickhouse/oid/date'
|
7
8
|
require 'active_record/connection_adapters/clickhouse/oid/date_time'
|
8
9
|
require 'active_record/connection_adapters/clickhouse/oid/big_integer'
|
@@ -17,9 +18,22 @@ module ActiveRecord
|
|
17
18
|
# Establishes a connection to the database that's used by all Active Record objects
|
18
19
|
def clickhouse_connection(config)
|
19
20
|
config = config.symbolize_keys
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
|
22
|
+
if config[:connection]
|
23
|
+
connection = {
|
24
|
+
connection: config[:connection]
|
25
|
+
}
|
26
|
+
else
|
27
|
+
port = config[:port] || 8123
|
28
|
+
connection = {
|
29
|
+
host: config[:host] || 'localhost',
|
30
|
+
port: port,
|
31
|
+
ssl: config[:ssl].present? ? config[:ssl] : port == 443,
|
32
|
+
sslca: config[:sslca],
|
33
|
+
read_timeout: config[:read_timeout],
|
34
|
+
write_timeout: config[:write_timeout],
|
35
|
+
}
|
36
|
+
end
|
23
37
|
|
24
38
|
if config.key?(:database)
|
25
39
|
database = config[:database]
|
@@ -27,7 +41,7 @@ module ActiveRecord
|
|
27
41
|
raise ArgumentError, 'No database specified. Missing argument: database.'
|
28
42
|
end
|
29
43
|
|
30
|
-
ConnectionAdapters::ClickhouseAdapter.new(logger,
|
44
|
+
ConnectionAdapters::ClickhouseAdapter.new(logger, connection, { user: config[:username], password: config[:password], database: database }.compact, config)
|
31
45
|
end
|
32
46
|
end
|
33
47
|
end
|
@@ -50,7 +64,11 @@ module ActiveRecord
|
|
50
64
|
module TypeCaster
|
51
65
|
class Map
|
52
66
|
def is_view
|
53
|
-
|
67
|
+
if @klass.respond_to?(:is_view)
|
68
|
+
@klass.is_view # rails 6.1
|
69
|
+
else
|
70
|
+
types.is_view # less than 6.1
|
71
|
+
end
|
54
72
|
end
|
55
73
|
end
|
56
74
|
end
|
@@ -79,7 +97,6 @@ module ActiveRecord
|
|
79
97
|
|
80
98
|
class ClickhouseAdapter < AbstractAdapter
|
81
99
|
ADAPTER_NAME = 'Clickhouse'.freeze
|
82
|
-
|
83
100
|
NATIVE_DATABASE_TYPES = {
|
84
101
|
string: { name: 'String' },
|
85
102
|
integer: { name: 'UInt32' },
|
@@ -88,7 +105,21 @@ module ActiveRecord
|
|
88
105
|
decimal: { name: 'Decimal' },
|
89
106
|
datetime: { name: 'DateTime' },
|
90
107
|
date: { name: 'Date' },
|
91
|
-
boolean: { name: 'UInt8' }
|
108
|
+
boolean: { name: 'UInt8' },
|
109
|
+
|
110
|
+
int8: { name: 'Int8' },
|
111
|
+
int16: { name: 'Int16' },
|
112
|
+
int32: { name: 'Int32' },
|
113
|
+
int64: { name: 'Int64' },
|
114
|
+
int128: { name: 'Int128' },
|
115
|
+
int256: { name: 'Int256' },
|
116
|
+
|
117
|
+
uint8: { name: 'UInt8' },
|
118
|
+
uint16: { name: 'UInt16' },
|
119
|
+
uint32: { name: 'UInt32' },
|
120
|
+
uint64: { name: 'UInt64' },
|
121
|
+
# uint128: { name: 'UInt128' }, not yet implemented in clickhouse
|
122
|
+
uint256: { name: 'UInt256' },
|
92
123
|
}.freeze
|
93
124
|
|
94
125
|
include Clickhouse::SchemaStatements
|
@@ -139,10 +170,12 @@ module ActiveRecord
|
|
139
170
|
when /(Nullable)?\(?String\)?/
|
140
171
|
super('String')
|
141
172
|
when /(Nullable)?\(?U?Int8\)?/
|
142
|
-
|
143
|
-
when /(Nullable)?\(?U?
|
144
|
-
|
145
|
-
when /(Nullable)?\(?U?
|
173
|
+
1
|
174
|
+
when /(Nullable)?\(?U?Int16\)?/
|
175
|
+
2
|
176
|
+
when /(Nullable)?\(?U?Int32\)?/
|
177
|
+
nil
|
178
|
+
when /(Nullable)?\(?U?Int64\)?/
|
146
179
|
8
|
147
180
|
else
|
148
181
|
super
|
@@ -154,14 +187,24 @@ module ActiveRecord
|
|
154
187
|
register_class_with_limit m, %r(String), Type::String
|
155
188
|
register_class_with_limit m, 'Date', Clickhouse::OID::Date
|
156
189
|
register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
|
157
|
-
|
158
|
-
m.alias_type 'UInt16', 'UInt8'
|
159
|
-
m.alias_type 'UInt32', 'UInt8'
|
160
|
-
register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
|
190
|
+
|
161
191
|
register_class_with_limit m, %r(Int8), Type::Integer
|
162
|
-
m
|
163
|
-
m
|
192
|
+
register_class_with_limit m, %r(Int16), Type::Integer
|
193
|
+
register_class_with_limit m, %r(Int32), Type::Integer
|
164
194
|
register_class_with_limit m, %r(Int64), Type::Integer
|
195
|
+
register_class_with_limit m, %r(Int128), Type::Integer
|
196
|
+
register_class_with_limit m, %r(Int256), Type::Integer
|
197
|
+
|
198
|
+
register_class_with_limit m, %r(UInt8), Type::UnsignedInteger
|
199
|
+
register_class_with_limit m, %r(UInt16), Type::UnsignedInteger
|
200
|
+
register_class_with_limit m, %r(UInt32), Type::UnsignedInteger
|
201
|
+
register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
|
202
|
+
#register_class_with_limit m, %r(UInt128), Type::UnsignedInteger #not implemnted in clickhouse
|
203
|
+
register_class_with_limit m, %r(UInt256), Type::UnsignedInteger
|
204
|
+
# register_class_with_limit m, %r(Array), Clickhouse::OID::Array
|
205
|
+
m.register_type(%r(Array)) do |sql_type|
|
206
|
+
Clickhouse::OID::Array.new(sql_type)
|
207
|
+
end
|
165
208
|
end
|
166
209
|
|
167
210
|
# Quoting time without microseconds
|
@@ -201,6 +244,12 @@ module ActiveRecord
|
|
201
244
|
ClickhouseActiverecord::SchemaDumper.create(self, options)
|
202
245
|
end
|
203
246
|
|
247
|
+
# @param [String] table
|
248
|
+
# @return [String]
|
249
|
+
def show_create_table(table)
|
250
|
+
do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first).gsub(/[\n\s]+/m, ' ')
|
251
|
+
end
|
252
|
+
|
204
253
|
# Create a new ClickHouse database.
|
205
254
|
def create_database(name)
|
206
255
|
sql = apply_cluster "CREATE DATABASE #{quote_table_name(name)}"
|
@@ -213,7 +262,7 @@ module ActiveRecord
|
|
213
262
|
def create_view(table_name, **options)
|
214
263
|
options.merge!(view: true)
|
215
264
|
options = apply_replica(table_name, options)
|
216
|
-
td = create_table_definition(apply_cluster(table_name), options)
|
265
|
+
td = create_table_definition(apply_cluster(table_name), **options)
|
217
266
|
yield td if block_given?
|
218
267
|
|
219
268
|
if options[:force]
|
@@ -223,10 +272,10 @@ module ActiveRecord
|
|
223
272
|
execute schema_creation.accept td
|
224
273
|
end
|
225
274
|
|
226
|
-
def create_table(table_name, **options)
|
275
|
+
def create_table(table_name, **options, &block)
|
227
276
|
options = apply_replica(table_name, options)
|
228
|
-
td = create_table_definition(apply_cluster(table_name), options)
|
229
|
-
|
277
|
+
td = create_table_definition(apply_cluster(table_name), **options)
|
278
|
+
block.call td if block_given?
|
230
279
|
|
231
280
|
if options[:force]
|
232
281
|
drop_table(table_name, options.merge(if_exists: true))
|
@@ -235,6 +284,19 @@ module ActiveRecord
|
|
235
284
|
execute schema_creation.accept td
|
236
285
|
end
|
237
286
|
|
287
|
+
def create_table_with_distributed(table_name, **options, &block)
|
288
|
+
sharding_key = options.delete(:sharding_key) || 'rand()'
|
289
|
+
create_table("#{table_name}_distributed", **options, &block)
|
290
|
+
raise 'Set a cluster' unless cluster
|
291
|
+
|
292
|
+
distributed_options = "Distributed(#{cluster},#{@config[:database]},#{table_name}_distributed,#{sharding_key})"
|
293
|
+
create_table(table_name, **options.merge(options: distributed_options), &block)
|
294
|
+
end
|
295
|
+
|
296
|
+
def drop_table_with_distributed(table_name, **options)
|
297
|
+
["#{table_name}_distributed", table_name].each { |name| drop_table(name, **options) }
|
298
|
+
end
|
299
|
+
|
238
300
|
# Drops a ClickHouse database.
|
239
301
|
def drop_database(name) #:nodoc:
|
240
302
|
sql = apply_cluster "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
|
@@ -275,14 +337,41 @@ module ActiveRecord
|
|
275
337
|
@full_config[:replica_name]
|
276
338
|
end
|
277
339
|
|
340
|
+
def use_default_replicated_merge_tree_params?
|
341
|
+
database_engine_atomic? && @full_config[:use_default_replicated_merge_tree_params]
|
342
|
+
end
|
343
|
+
|
344
|
+
def use_replica?
|
345
|
+
(replica || use_default_replicated_merge_tree_params?) && cluster
|
346
|
+
end
|
347
|
+
|
278
348
|
def replica_path(table)
|
279
349
|
"/clickhouse/tables/#{cluster}/#{@config[:database]}.#{table}"
|
280
350
|
end
|
281
351
|
|
352
|
+
def database_engine_atomic?
|
353
|
+
current_database_engine = "select engine from system.databases where name = '#{@config[:database]}'"
|
354
|
+
res = ActiveRecord::Base.connection.select_one(current_database_engine)
|
355
|
+
res['engine'] == 'Atomic' if res
|
356
|
+
end
|
357
|
+
|
282
358
|
def apply_cluster(sql)
|
283
359
|
cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
|
284
360
|
end
|
285
361
|
|
362
|
+
def supports_insert_on_duplicate_skip?
|
363
|
+
true
|
364
|
+
end
|
365
|
+
|
366
|
+
def supports_insert_on_duplicate_update?
|
367
|
+
true
|
368
|
+
end
|
369
|
+
|
370
|
+
def build_insert_sql(insert) # :nodoc:
|
371
|
+
sql = +"INSERT #{insert.into} #{insert.values_list}"
|
372
|
+
sql
|
373
|
+
end
|
374
|
+
|
286
375
|
protected
|
287
376
|
|
288
377
|
def last_inserted_id(result)
|
@@ -298,18 +387,36 @@ module ActiveRecord
|
|
298
387
|
private
|
299
388
|
|
300
389
|
def connect
|
301
|
-
@connection = Net::HTTP.start(@connection_parameters[
|
390
|
+
@connection = @connection_parameters[:connection] || Net::HTTP.start(@connection_parameters[:host], @connection_parameters[:port], use_ssl: @connection_parameters[:ssl], verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
391
|
+
|
392
|
+
@connection.ca_file = @connection_parameters[:ca_file] if @connection_parameters[:ca_file]
|
393
|
+
@connection.read_timeout = @connection_parameters[:read_timeout] if @connection_parameters[:read_timeout]
|
394
|
+
@connection.write_timeout = @connection_parameters[:write_timeout] if @connection_parameters[:write_timeout]
|
395
|
+
|
396
|
+
@connection
|
302
397
|
end
|
303
398
|
|
304
399
|
def apply_replica(table, options)
|
305
|
-
if
|
306
|
-
|
307
|
-
|
308
|
-
options[:options] = "Replicated#{match[1]}(#{([replica_path(table), replica].map{|v| "'#{v}'"} + [match[2].presence]).compact.join(', ')})#{match[3]}"
|
400
|
+
if use_replica? && options[:options]
|
401
|
+
if options[:options].match(/^Replicated/)
|
402
|
+
raise 'Do not try create Replicated table. It will be configured based on the *MergeTree engine.'
|
309
403
|
end
|
404
|
+
|
405
|
+
options[:options] = configure_replica(table, options[:options])
|
310
406
|
end
|
311
407
|
options
|
312
408
|
end
|
409
|
+
|
410
|
+
def configure_replica(table, options)
|
411
|
+
match = options.match(/^(.*?MergeTree)(?:\(([^\)]*)\))?((?:.|\n)*)/)
|
412
|
+
return options unless match
|
413
|
+
|
414
|
+
if replica
|
415
|
+
engine_params = ([replica_path(table), replica].map { |v| "'#{v}'" } + [match[2].presence]).compact.join(', ')
|
416
|
+
end
|
417
|
+
|
418
|
+
"Replicated#{match[1]}(#{engine_params})#{match[3]}"
|
419
|
+
end
|
313
420
|
end
|
314
421
|
end
|
315
422
|
end
|
@@ -1,17 +1,28 @@
|
|
1
|
+
require 'active_record/migration'
|
2
|
+
|
1
3
|
module ClickhouseActiverecord
|
2
4
|
|
3
5
|
class SchemaMigration < ::ActiveRecord::SchemaMigration
|
4
6
|
class << self
|
5
7
|
|
6
8
|
def create_table
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
return if table_exists?
|
10
|
+
|
11
|
+
version_options = connection.internal_string_options_for_primary_key
|
12
|
+
table_options = {
|
13
|
+
id: false, options: 'ReplacingMergeTree(ver) PARTITION BY version ORDER BY (version)', if_not_exists: true
|
14
|
+
}
|
15
|
+
if connection.instance_variable_get(:@full_config)[:distributed_service_tables]
|
16
|
+
table_options.merge!(sharding_key: 'cityHash64(version)')
|
17
|
+
table_creation_method = 'create_table_with_distributed'
|
18
|
+
else
|
19
|
+
table_creation_method = 'create_table'
|
20
|
+
end
|
21
|
+
|
22
|
+
connection.public_send(table_creation_method, table_name, **table_options) do |t|
|
23
|
+
t.string :version, **version_options
|
24
|
+
t.column :active, 'Int8', null: false, default: '1'
|
25
|
+
t.datetime :ver, null: false, default: -> { 'now()' }
|
15
26
|
end
|
16
27
|
end
|
17
28
|
|
@@ -24,14 +35,25 @@ module ClickhouseActiverecord
|
|
24
35
|
class InternalMetadata < ::ActiveRecord::InternalMetadata
|
25
36
|
class << self
|
26
37
|
def create_table
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
38
|
+
return if table_exists?
|
39
|
+
|
40
|
+
key_options = connection.internal_string_options_for_primary_key
|
41
|
+
table_options = {
|
42
|
+
id: false,
|
43
|
+
options: connection.adapter_name.downcase == 'clickhouse' ? 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)' : '',
|
44
|
+
if_not_exists: true
|
45
|
+
}
|
46
|
+
if connection.instance_variable_get(:@full_config).try(:[], :distributed_service_tables)
|
47
|
+
table_options.merge!(sharding_key: 'cityHash64(created_at)')
|
48
|
+
table_creation_method = 'create_table_with_distributed'
|
49
|
+
else
|
50
|
+
table_creation_method = 'create_table'
|
51
|
+
end
|
52
|
+
|
53
|
+
connection.public_send(table_creation_method, table_name, **table_options) do |t|
|
54
|
+
t.string :key, **key_options
|
55
|
+
t.string :value
|
56
|
+
t.timestamps
|
35
57
|
end
|
36
58
|
end
|
37
59
|
end
|
@@ -45,6 +67,16 @@ module ClickhouseActiverecord
|
|
45
67
|
@schema_migration = schema_migration
|
46
68
|
end
|
47
69
|
|
70
|
+
def up(target_version = nil)
|
71
|
+
selected_migrations = if block_given?
|
72
|
+
migrations.select { |m| yield m }
|
73
|
+
else
|
74
|
+
migrations
|
75
|
+
end
|
76
|
+
|
77
|
+
ClickhouseActiverecord::Migrator.new(:up, selected_migrations, schema_migration, target_version).migrate
|
78
|
+
end
|
79
|
+
|
48
80
|
def down(target_version = nil)
|
49
81
|
selected_migrations = if block_given?
|
50
82
|
migrations.select { |m| yield m }
|
@@ -33,16 +33,24 @@ module ClickhouseActiverecord
|
|
33
33
|
HEADER
|
34
34
|
end
|
35
35
|
|
36
|
+
def tables(stream)
|
37
|
+
sorted_tables = @connection.tables.sort {|a,b| @connection.show_create_table(a).match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : a <=> b }
|
38
|
+
|
39
|
+
sorted_tables.each do |table_name|
|
40
|
+
table(table_name, stream) unless ignored?(table_name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
36
44
|
def table(table, stream)
|
37
|
-
if table.match(/^\.inner
|
45
|
+
if table.match(/^\.inner/).nil?
|
38
46
|
unless simple
|
39
47
|
stream.puts " # TABLE: #{table}"
|
40
|
-
sql = @connection.
|
48
|
+
sql = @connection.show_create_table(table)
|
41
49
|
stream.puts " # SQL: #{sql.gsub(/ENGINE = Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "ENGINE = \\1(\\2)")}" if sql
|
42
50
|
# super(table.gsub(/^\.inner\./, ''), stream)
|
43
51
|
|
44
52
|
# detect view table
|
45
|
-
match = sql.match(/^CREATE\s+(MATERIALIZED
|
53
|
+
match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/)
|
46
54
|
end
|
47
55
|
|
48
56
|
# Copy from original dumper
|
@@ -125,5 +133,26 @@ HEADER
|
|
125
133
|
super
|
126
134
|
end
|
127
135
|
end
|
136
|
+
|
137
|
+
def schema_limit(column)
|
138
|
+
return nil if column.type == :float
|
139
|
+
super
|
140
|
+
end
|
141
|
+
|
142
|
+
def schema_unsigned(column)
|
143
|
+
return nil unless column.type == :integer && !simple
|
144
|
+
(column.sql_type =~ /(Nullable)?\(?UInt\d+\)?/).nil? ? false : nil
|
145
|
+
end
|
146
|
+
|
147
|
+
def schema_array(column)
|
148
|
+
(column.sql_type =~ /Array?\(/).nil? ? nil : true
|
149
|
+
end
|
150
|
+
|
151
|
+
def prepare_column_options(column)
|
152
|
+
spec = {}
|
153
|
+
spec[:unsigned] = schema_unsigned(column)
|
154
|
+
spec[:array] = schema_array(column)
|
155
|
+
spec.merge(super).compact
|
156
|
+
end
|
128
157
|
end
|
129
158
|
end
|
@@ -6,7 +6,7 @@ module ClickhouseActiverecord
|
|
6
6
|
delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base
|
7
7
|
|
8
8
|
def initialize(configuration)
|
9
|
-
@configuration = configuration
|
9
|
+
@configuration = configuration.with_indifferent_access
|
10
10
|
end
|
11
11
|
|
12
12
|
def create
|
@@ -43,7 +43,15 @@ module ClickhouseActiverecord
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def structure_load(*args)
|
46
|
-
File.read(args.first).split(";\n\n").each
|
46
|
+
File.read(args.first).split(";\n\n").each do |sql|
|
47
|
+
if sql.gsub(/[a-z]/i, '').blank?
|
48
|
+
next
|
49
|
+
elsif sql =~ /^INSERT INTO/
|
50
|
+
connection.do_execute(sql, nil, format: nil)
|
51
|
+
else
|
52
|
+
connection.execute(sql)
|
53
|
+
end
|
54
|
+
end
|
47
55
|
end
|
48
56
|
|
49
57
|
def migrate
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
require 'active_record/connection_adapters/clickhouse_adapter'
|
4
4
|
|
5
|
+
require_relative '../core_extensions/active_record/migration/command_recorder'
|
6
|
+
ActiveRecord::Migration::CommandRecorder.include CoreExtensions::ActiveRecord::Migration::CommandRecorder
|
7
|
+
|
5
8
|
if defined?(Rails::Railtie)
|
6
9
|
require 'clickhouse-activerecord/railtie'
|
7
10
|
require 'clickhouse-activerecord/schema'
|
@@ -13,7 +13,7 @@ class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerato
|
|
13
13
|
|
14
14
|
def db_migrate_path
|
15
15
|
if defined?(Rails.application) && Rails.application && respond_to?(:configured_migrate_path, true)
|
16
|
-
configured_migrate_path
|
16
|
+
configured_migrate_path || default_migrate_path
|
17
17
|
else
|
18
18
|
default_migrate_path
|
19
19
|
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.5.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Odintsov
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -112,6 +112,8 @@ files:
|
|
112
112
|
- bin/console
|
113
113
|
- bin/setup
|
114
114
|
- clickhouse-activerecord.gemspec
|
115
|
+
- core_extensions/active_record/migration/command_recorder.rb
|
116
|
+
- lib/active_record/connection_adapters/clickhouse/oid/array.rb
|
115
117
|
- lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
|
116
118
|
- lib/active_record/connection_adapters/clickhouse/oid/date.rb
|
117
119
|
- lib/active_record/connection_adapters/clickhouse/oid/date_time.rb
|
@@ -134,7 +136,7 @@ homepage: https://github.com/pnixx/clickhouse-activerecord
|
|
134
136
|
licenses:
|
135
137
|
- MIT
|
136
138
|
metadata: {}
|
137
|
-
post_install_message:
|
139
|
+
post_install_message:
|
138
140
|
rdoc_options: []
|
139
141
|
require_paths:
|
140
142
|
- lib
|
@@ -149,8 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
151
|
- !ruby/object:Gem::Version
|
150
152
|
version: '0'
|
151
153
|
requirements: []
|
152
|
-
rubygems_version: 3.0.
|
153
|
-
signing_key:
|
154
|
+
rubygems_version: 3.0.3
|
155
|
+
signing_key:
|
154
156
|
specification_version: 4
|
155
157
|
summary: ClickHouse ActiveRecord
|
156
158
|
test_files: []
|