clickhouse-activerecord 0.5.7 → 0.6.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 +4 -4
- data/CHANGELOG.md +8 -2
- data/README.md +14 -2
- data/clickhouse-activerecord.gemspec +1 -2
- data/core_extensions/active_record/migration/command_recorder.rb +0 -9
- data/lib/active_record/connection_adapters/clickhouse/oid/array.rb +36 -0
- data/lib/active_record/connection_adapters/clickhouse/oid/date_time.rb +1 -2
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +15 -2
- data/lib/active_record/connection_adapters/clickhouse/schema_definitions.rb +32 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +18 -8
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +96 -45
- data/lib/arel/nodes/settings.rb +11 -0
- data/lib/arel/nodes/using.rb +6 -0
- data/lib/arel/visitors/clickhouse.rb +60 -0
- data/lib/clickhouse-activerecord/migration.rb +37 -15
- data/lib/clickhouse-activerecord/railtie.rb +6 -0
- data/lib/clickhouse-activerecord/tasks.rb +1 -1
- data/lib/clickhouse-activerecord/version.rb +1 -1
- data/lib/clickhouse-activerecord.rb +12 -0
- data/lib/core_extensions/active_record/relation.rb +44 -0
- data/lib/core_extensions/arel/nodes/select_statement.rb +18 -0
- data/lib/core_extensions/arel/select_manager.rb +17 -0
- data/lib/{clickhouse-activerecord → core_extensions}/arel/table.rb +4 -2
- metadata +18 -21
- data/lib/clickhouse-activerecord/arel/visitors/to_sql.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74728f97bde396aae27436dd6b7ec8d57ec172e818122f9094d994583c92781f
|
4
|
+
data.tar.gz: 64e08f74a95d755338b489b32f2aade49685b6b604a3d02d2f232ca9f24b7b23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 959c753aaf61bcef02dd336953d1bd88bc0d2d3762bc5f96e3816699685c809da2f4ad7646201570892a12fad32760a80d69ff6292b172fb6e9c518c13dc622d
|
7
|
+
data.tar.gz: e7ee279cf5dc1b8168448f8c657550e2cb418121da1ef58c1d53989f8a7cc678527fcb7678cc0ba903d9e5d9e108450c9da28d03ba0e6d9364738d1c2f60e414
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
+
### Version 0.5.10 (Jun 22, 2022)
|
2
|
+
|
3
|
+
* Fixes to create_table method (#70)
|
4
|
+
* Added support for rails 7 (#65)
|
5
|
+
* Use ClickHouse default KeepAlive timeout of 10 seconds (#67)
|
6
|
+
|
1
7
|
### Version 0.5.6 (Oct 25, 2021)
|
2
|
-
|
8
|
+
|
3
9
|
* Added auto creating service distributed tables and additional options for creating view [@ygreeek](https://github.com/ygreeek)
|
4
10
|
* Added default user agent
|
5
11
|
|
6
12
|
### Version 0.5.3 (Sep 22, 2021)
|
7
|
-
|
13
|
+
|
8
14
|
* Fix replica cluster for a new syntax MergeTree
|
9
15
|
* Fix support rails 5.2 on alter table
|
10
16
|
* Support array type of column
|
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 22.0 LTS.
|
5
5
|
|
6
6
|
## Installation
|
7
7
|
|
@@ -165,7 +165,7 @@ Structure load from `db/clickhouse_structure.sql` file:
|
|
165
165
|
|
166
166
|
```ruby
|
167
167
|
Action.where(url: 'http://example.com', date: Date.current).where.not(name: nil).order(created_at: :desc).limit(10)
|
168
|
-
# Clickhouse Action Load (10.3ms) SELECT
|
168
|
+
# 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
|
169
169
|
#=> #<ActiveRecord::Relation [#<Action *** >]>
|
170
170
|
|
171
171
|
Action.create(url: 'http://example.com', date: Date.yesterday)
|
@@ -175,6 +175,18 @@ Action.create(url: 'http://example.com', date: Date.yesterday)
|
|
175
175
|
ActionView.maximum(:date)
|
176
176
|
# Clickhouse (10.3ms) SELECT maxMerge(actions.date) FROM actions
|
177
177
|
#=> 'Wed, 29 Nov 2017'
|
178
|
+
|
179
|
+
Action.where(date: Date.current).final.limit(10)
|
180
|
+
# Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions FINAL WHERE actions.date = '2017-11-29' LIMIT 10
|
181
|
+
#=> #<ActiveRecord::Relation [#<Action *** >]>
|
182
|
+
|
183
|
+
Action.settings(optimize_read_in_order: 1).where(date: Date.current).limit(10)
|
184
|
+
# Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions FINAL WHERE actions.date = '2017-11-29' LIMIT 10 SETTINGS optimize_read_in_order = 1
|
185
|
+
#=> #<ActiveRecord::Relation [#<Action *** >]>
|
186
|
+
|
187
|
+
User.joins(:actions).using(:group_id)
|
188
|
+
# Clickhouse User Load (10.3ms) SELECT users.* FROM users INNER JOIN actions USING group_id
|
189
|
+
#=> #<ActiveRecord::Relation [#<Action *** >]>
|
178
190
|
```
|
179
191
|
|
180
192
|
|
@@ -24,9 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.require_paths = ['lib']
|
25
25
|
|
26
26
|
spec.add_runtime_dependency 'bundler', '>= 1.13.4'
|
27
|
-
spec.add_runtime_dependency 'activerecord', '>= 5.2'
|
27
|
+
spec.add_runtime_dependency 'activerecord', '>= 5.2', '< 7'
|
28
28
|
|
29
|
-
spec.add_development_dependency 'bundler', '~> 1.15'
|
30
29
|
spec.add_development_dependency 'rake', '~> 13.0'
|
31
30
|
spec.add_development_dependency 'rspec', '~> 3.4'
|
32
31
|
spec.add_development_dependency 'pry', '~> 0.12'
|
@@ -2,21 +2,12 @@ module CoreExtensions
|
|
2
2
|
module ActiveRecord
|
3
3
|
module Migration
|
4
4
|
module CommandRecorder
|
5
|
-
def create_table_with_distributed(*args, &block)
|
6
|
-
record(:create_table_with_distributed, args, &block)
|
7
|
-
end
|
8
|
-
|
9
5
|
def create_view(*args, &block)
|
10
6
|
record(:create_view, args, &block)
|
11
7
|
end
|
12
8
|
|
13
9
|
private
|
14
10
|
|
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
11
|
def invert_create_view(args)
|
21
12
|
view_name, options = args
|
22
13
|
[:drop_table, view_name, options]
|
@@ -23,6 +23,42 @@ module ActiveRecord
|
|
23
23
|
@subtype
|
24
24
|
end
|
25
25
|
|
26
|
+
def deserialize(value)
|
27
|
+
if value.is_a?(::Array)
|
28
|
+
value.map { |item| deserialize(item) }
|
29
|
+
else
|
30
|
+
return value if value.nil?
|
31
|
+
case @subtype
|
32
|
+
when :integer
|
33
|
+
value.to_i
|
34
|
+
when :datetime
|
35
|
+
::DateTime.parse(value)
|
36
|
+
when :date
|
37
|
+
::Date.parse(value)
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def serialize(value)
|
45
|
+
if value.is_a?(::Array)
|
46
|
+
value.map { |item| serialize(item) }
|
47
|
+
else
|
48
|
+
return value if value.nil?
|
49
|
+
case @subtype
|
50
|
+
when :integer
|
51
|
+
value.to_i
|
52
|
+
when :datetime
|
53
|
+
DateTime.new.serialize(value)
|
54
|
+
when :date
|
55
|
+
Date.new.serialize(value)
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
26
62
|
end
|
27
63
|
end
|
28
64
|
end
|
@@ -9,9 +9,8 @@ module ActiveRecord
|
|
9
9
|
def serialize(value)
|
10
10
|
value = super
|
11
11
|
return unless value
|
12
|
-
return value.strftime('%Y-%m-%d %H:%M:%S') unless value.acts_like?(:time)
|
13
12
|
|
14
|
-
value.
|
13
|
+
value.strftime('%Y-%m-%d %H:%M:%S' + (@precision.present? && @precision > 0 ? ".%#{@precision}N" : ''))
|
15
14
|
end
|
16
15
|
|
17
16
|
def type_cast_from_database(value)
|
@@ -19,9 +19,18 @@ module ActiveRecord
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def add_column_options!(sql, options)
|
22
|
+
if options[:value]
|
23
|
+
sql.gsub!(/\s+(.*)/, " \\1(#{options[:value]})")
|
24
|
+
end
|
25
|
+
if options[:fixed_string]
|
26
|
+
sql.gsub!(/\s+(.*)/, " FixedString(#{options[:fixed_string]})")
|
27
|
+
end
|
22
28
|
if options[:null] || options[:null].nil?
|
23
29
|
sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
|
24
30
|
end
|
31
|
+
if options[:low_cardinality]
|
32
|
+
sql.gsub!(/\s+(.*)/, ' LowCardinality(\1)')
|
33
|
+
end
|
25
34
|
if options[:array]
|
26
35
|
sql.gsub!(/\s+(.*)/, ' Array(\1)')
|
27
36
|
end
|
@@ -73,7 +82,7 @@ module ActiveRecord
|
|
73
82
|
return unless match
|
74
83
|
return if match[:database]
|
75
84
|
|
76
|
-
create_sql << "TO #{current_database}.#{
|
85
|
+
create_sql << "TO #{current_database}.#{match[:table_name].sub('.', '')}"
|
77
86
|
end
|
78
87
|
|
79
88
|
def visit_TableDefinition(o)
|
@@ -116,7 +125,11 @@ module ActiveRecord
|
|
116
125
|
end
|
117
126
|
|
118
127
|
def current_database
|
119
|
-
ActiveRecord::
|
128
|
+
if ActiveRecord::version >= Gem::Version.new('6.1')
|
129
|
+
ActiveRecord::Base.connection_db_config.database
|
130
|
+
else
|
131
|
+
ActiveRecord::Base.connection_config[:database]
|
132
|
+
end
|
120
133
|
end
|
121
134
|
end
|
122
135
|
end
|
@@ -62,6 +62,38 @@ module ActiveRecord
|
|
62
62
|
end
|
63
63
|
args.each { |name| column(name, kind, **options.except(:limit, :unsigned)) }
|
64
64
|
end
|
65
|
+
|
66
|
+
def datetime(*args, **options)
|
67
|
+
kind = :datetime
|
68
|
+
|
69
|
+
if options[:precision]
|
70
|
+
kind = :datetime64
|
71
|
+
options[:value] = options[:precision]
|
72
|
+
end
|
73
|
+
|
74
|
+
args.each { |name| column(name, kind, **options.except(:precision)) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def uuid(*args, **options)
|
78
|
+
args.each { |name| column(name, :uuid, **options) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def enum(*args, **options)
|
82
|
+
kind = :enum8
|
83
|
+
|
84
|
+
unless options[:value].is_a? Hash
|
85
|
+
raise ArgumentError, "Column #{args.first}: option 'value' must be Hash, got: #{options[:value].class}"
|
86
|
+
end
|
87
|
+
|
88
|
+
options[:value] = options[:value].each_with_object([]) { |(k, v), arr| arr.push("'#{k}' = #{v}") }.join(', ')
|
89
|
+
|
90
|
+
if options[:limit]
|
91
|
+
kind = :enum8 if options[:limit] == 1
|
92
|
+
kind = :enum16 if options[:limit] == 2
|
93
|
+
end
|
94
|
+
|
95
|
+
args.each { |name| column(name, kind, **options.except(:limit)) }
|
96
|
+
end
|
65
97
|
end
|
66
98
|
end
|
67
99
|
end
|
@@ -6,8 +6,8 @@ module ActiveRecord
|
|
6
6
|
module ConnectionAdapters
|
7
7
|
module Clickhouse
|
8
8
|
module SchemaStatements
|
9
|
-
def execute(sql, name = nil)
|
10
|
-
do_execute(sql, name)
|
9
|
+
def execute(sql, name = nil, settings: {})
|
10
|
+
do_execute(sql, name, settings: settings)
|
11
11
|
end
|
12
12
|
|
13
13
|
def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil)
|
@@ -18,7 +18,7 @@ module ActiveRecord
|
|
18
18
|
|
19
19
|
def exec_query(sql, name = nil, binds = [], prepare: false)
|
20
20
|
result = do_execute(sql, name)
|
21
|
-
ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
|
21
|
+
ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'], result['meta'].map { |m| [m['name'], type_map.lookup(m['type'])] }.to_h)
|
22
22
|
rescue ActiveRecord::ActiveRecordError => e
|
23
23
|
raise e
|
24
24
|
rescue StandardError => e
|
@@ -39,7 +39,7 @@ module ActiveRecord
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def tables(name = nil)
|
42
|
-
result = do_system_execute(
|
42
|
+
result = do_system_execute("SHOW TABLES WHERE name NOT LIKE '.inner_id.%'", name)
|
43
43
|
return [] if result.nil?
|
44
44
|
result['data'].flatten
|
45
45
|
end
|
@@ -92,7 +92,7 @@ module ActiveRecord
|
|
92
92
|
if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
|
93
93
|
raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
|
94
94
|
end
|
95
|
-
|
95
|
+
do_execute(insert_versions_sql(inserting), nil, settings: {max_partitions_per_insert_block: [100, inserting.size].max})
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
@@ -105,10 +105,20 @@ module ActiveRecord
|
|
105
105
|
def process_response(res)
|
106
106
|
case res.code.to_i
|
107
107
|
when 200
|
108
|
-
res.body.
|
108
|
+
if res.body.to_s.include?("DB::Exception")
|
109
|
+
raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
|
110
|
+
else
|
111
|
+
res.body.presence && JSON.parse(res.body)
|
112
|
+
end
|
109
113
|
else
|
110
|
-
|
111
|
-
|
114
|
+
case res.body
|
115
|
+
when /DB::Exception:.*\(UNKNOWN_DATABASE\)/
|
116
|
+
raise ActiveRecord::NoDatabaseError
|
117
|
+
when /DB::Exception:.*\(DATABASE_ALREADY_EXISTS\)/
|
118
|
+
raise ActiveRecord::DatabaseAlreadyExists
|
119
|
+
else
|
120
|
+
raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
|
121
|
+
end
|
112
122
|
end
|
113
123
|
rescue JSON::ParserError
|
114
124
|
res.body
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require '
|
3
|
+
require 'arel/visitors/clickhouse'
|
4
|
+
require 'arel/nodes/settings'
|
5
|
+
require 'arel/nodes/using'
|
5
6
|
require 'clickhouse-activerecord/migration'
|
6
7
|
require 'active_record/connection_adapters/clickhouse/oid/array'
|
7
8
|
require 'active_record/connection_adapters/clickhouse/oid/date'
|
@@ -11,6 +12,7 @@ require 'active_record/connection_adapters/clickhouse/schema_definitions'
|
|
11
12
|
require 'active_record/connection_adapters/clickhouse/schema_creation'
|
12
13
|
require 'active_record/connection_adapters/clickhouse/schema_statements'
|
13
14
|
require 'net/http'
|
15
|
+
require 'openssl'
|
14
16
|
|
15
17
|
module ActiveRecord
|
16
18
|
class Base
|
@@ -32,6 +34,7 @@ module ActiveRecord
|
|
32
34
|
sslca: config[:sslca],
|
33
35
|
read_timeout: config[:read_timeout],
|
34
36
|
write_timeout: config[:write_timeout],
|
37
|
+
keep_alive_timeout: config[:keep_alive_timeout]
|
35
38
|
}
|
36
39
|
end
|
37
40
|
|
@@ -46,21 +49,6 @@ module ActiveRecord
|
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
49
|
-
class Relation
|
50
|
-
|
51
|
-
# Replace for only ClickhouseAdapter
|
52
|
-
def reverse_order!
|
53
|
-
orders = order_values.uniq
|
54
|
-
orders.reject!(&:blank?)
|
55
|
-
if self.connection.is_a?(ConnectionAdapters::ClickhouseAdapter) && orders.empty? && !primary_key
|
56
|
-
self.order_values = %w(date created_at).select {|c| column_names.include?(c) }.map{|c| arel_attribute(c).desc }
|
57
|
-
else
|
58
|
-
self.order_values = reverse_sql_order(orders)
|
59
|
-
end
|
60
|
-
self
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
52
|
module TypeCaster
|
65
53
|
class Map
|
66
54
|
def is_view
|
@@ -74,7 +62,9 @@ module ActiveRecord
|
|
74
62
|
end
|
75
63
|
|
76
64
|
module ModelSchema
|
77
|
-
|
65
|
+
module ClassMethods
|
66
|
+
delegate :final, :settings, to: :all
|
67
|
+
|
78
68
|
def is_view
|
79
69
|
@is_view || false
|
80
70
|
end
|
@@ -82,13 +72,12 @@ module ActiveRecord
|
|
82
72
|
def is_view=(value)
|
83
73
|
@is_view = value
|
84
74
|
end
|
85
|
-
|
86
|
-
def arel_table # :nodoc:
|
87
|
-
|
88
|
-
end
|
89
|
-
|
75
|
+
#
|
76
|
+
# def arel_table # :nodoc:
|
77
|
+
# @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
|
78
|
+
# end
|
90
79
|
end
|
91
|
-
|
80
|
+
end
|
92
81
|
|
93
82
|
module ConnectionAdapters
|
94
83
|
class ClickhouseColumn < Column
|
@@ -104,8 +93,13 @@ module ActiveRecord
|
|
104
93
|
float: { name: 'Float32' },
|
105
94
|
decimal: { name: 'Decimal' },
|
106
95
|
datetime: { name: 'DateTime' },
|
96
|
+
datetime64: { name: 'DateTime64' },
|
107
97
|
date: { name: 'Date' },
|
108
|
-
boolean: { name: '
|
98
|
+
boolean: { name: 'Bool' },
|
99
|
+
uuid: { name: 'UUID' },
|
100
|
+
|
101
|
+
enum8: { name: 'Enum8' },
|
102
|
+
enum16: { name: 'Enum16' },
|
109
103
|
|
110
104
|
int8: { name: 'Int8' },
|
111
105
|
int16: { name: 'Int16' },
|
@@ -154,7 +148,7 @@ module ActiveRecord
|
|
154
148
|
end
|
155
149
|
|
156
150
|
def arel_visitor # :nodoc:
|
157
|
-
|
151
|
+
Arel::Visitors::Clickhouse.new(self)
|
158
152
|
end
|
159
153
|
|
160
154
|
def native_database_types #:nodoc:
|
@@ -182,11 +176,25 @@ module ActiveRecord
|
|
182
176
|
end
|
183
177
|
end
|
184
178
|
|
179
|
+
# `extract_scale` and `extract_precision` are the same as in the Rails abstract base class,
|
180
|
+
# except this permits a space after the comma
|
181
|
+
|
182
|
+
def extract_scale(sql_type)
|
183
|
+
case sql_type
|
184
|
+
when /\((\d+)\)/ then 0
|
185
|
+
when /\((\d+)(,\s?(\d+))\)/ then $3.to_i
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def extract_precision(sql_type)
|
190
|
+
$1.to_i if sql_type =~ /\((\d+)(,\s?\d+)?\)/
|
191
|
+
end
|
192
|
+
|
185
193
|
def initialize_type_map(m) # :nodoc:
|
186
194
|
super
|
187
195
|
register_class_with_limit m, %r(String), Type::String
|
188
196
|
register_class_with_limit m, 'Date', Clickhouse::OID::Date
|
189
|
-
|
197
|
+
register_class_with_precision m, %r(datetime)i, Clickhouse::OID::DateTime
|
190
198
|
|
191
199
|
register_class_with_limit m, %r(Int8), Type::Integer
|
192
200
|
register_class_with_limit m, %r(Int16), Type::Integer
|
@@ -207,17 +215,34 @@ module ActiveRecord
|
|
207
215
|
end
|
208
216
|
end
|
209
217
|
|
218
|
+
def _quote(value)
|
219
|
+
case value
|
220
|
+
when Array
|
221
|
+
'[' + value.map { |v| _quote(v) }.join(', ') + ']'
|
222
|
+
else
|
223
|
+
super
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
210
227
|
# Quoting time without microseconds
|
211
228
|
def quoted_date(value)
|
212
229
|
if value.acts_like?(:time)
|
213
|
-
|
230
|
+
if ActiveRecord::version >= Gem::Version.new('7')
|
231
|
+
zone_conversion_method = ActiveRecord.default_timezone == :utc ? :getutc : :getlocal
|
232
|
+
else
|
233
|
+
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
234
|
+
end
|
214
235
|
|
215
236
|
if value.respond_to?(zone_conversion_method)
|
216
237
|
value = value.send(zone_conversion_method)
|
217
238
|
end
|
218
239
|
end
|
219
240
|
|
220
|
-
|
241
|
+
if ActiveRecord::version >= Gem::Version.new('7')
|
242
|
+
value.to_fs(:db)
|
243
|
+
else
|
244
|
+
value.to_s(:db)
|
245
|
+
end
|
221
246
|
end
|
222
247
|
|
223
248
|
def column_name_for_operation(operation, node) # :nodoc:
|
@@ -269,32 +294,30 @@ module ActiveRecord
|
|
269
294
|
drop_table(table_name, options.merge(if_exists: true))
|
270
295
|
end
|
271
296
|
|
272
|
-
|
297
|
+
do_execute(schema_creation.accept(td), format: nil)
|
273
298
|
end
|
274
299
|
|
275
300
|
def create_table(table_name, **options, &block)
|
276
301
|
options = apply_replica(table_name, options)
|
277
302
|
td = create_table_definition(apply_cluster(table_name), **options)
|
278
303
|
block.call td if block_given?
|
304
|
+
td.column(:id, options[:id], null: false) if options[:id].present? && td[:id].blank?
|
279
305
|
|
280
306
|
if options[:force]
|
281
307
|
drop_table(table_name, options.merge(if_exists: true))
|
282
308
|
end
|
283
309
|
|
284
|
-
|
285
|
-
end
|
310
|
+
do_execute(schema_creation.accept(td), format: nil)
|
286
311
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
312
|
+
if options[:with_distributed]
|
313
|
+
distributed_table_name = options.delete(:with_distributed)
|
314
|
+
sharding_key = options.delete(:sharding_key) || 'rand()'
|
315
|
+
raise 'Set a cluster' unless cluster
|
291
316
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
def drop_table_with_distributed(table_name, **options)
|
297
|
-
["#{table_name}_distributed", table_name].each { |name| drop_table(name, **options) }
|
317
|
+
distributed_options =
|
318
|
+
"Distributed(#{cluster}, #{@config[:database]}, #{table_name}, #{sharding_key})"
|
319
|
+
create_table(distributed_table_name, **options.merge(options: distributed_options), &block)
|
320
|
+
end
|
298
321
|
end
|
299
322
|
|
300
323
|
# Drops a ClickHouse database.
|
@@ -312,10 +335,29 @@ module ActiveRecord
|
|
312
335
|
|
313
336
|
def drop_table(table_name, options = {}) # :nodoc:
|
314
337
|
do_execute apply_cluster "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
|
338
|
+
|
339
|
+
if options[:with_distributed]
|
340
|
+
distributed_table_name = options.delete(:with_distributed)
|
341
|
+
drop_table(distributed_table_name, **options)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def add_column(table_name, column_name, type, **options)
|
346
|
+
return if options[:if_not_exists] == true && column_exists?(table_name, column_name, type)
|
347
|
+
|
348
|
+
at = create_alter_table table_name
|
349
|
+
at.add_column(column_name, type, **options)
|
350
|
+
execute(schema_creation.accept(at), nil, settings: {wait_end_of_query: 1, send_progress_in_http_headers: 1})
|
351
|
+
end
|
352
|
+
|
353
|
+
def remove_column(table_name, column_name, type = nil, **options)
|
354
|
+
return if options[:if_exists] == true && !column_exists?(table_name, column_name)
|
355
|
+
|
356
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, **options)}", nil, settings: {wait_end_of_query: 1, send_progress_in_http_headers: 1})
|
315
357
|
end
|
316
358
|
|
317
359
|
def change_column(table_name, column_name, type, options = {})
|
318
|
-
result = do_execute
|
360
|
+
result = do_execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}", nil, settings: {wait_end_of_query: 1, send_progress_in_http_headers: 1})
|
319
361
|
raise "Error parse json response: #{result}" if result.presence && !result.is_a?(Hash)
|
320
362
|
end
|
321
363
|
|
@@ -351,12 +393,18 @@ module ActiveRecord
|
|
351
393
|
|
352
394
|
def database_engine_atomic?
|
353
395
|
current_database_engine = "select engine from system.databases where name = '#{@config[:database]}'"
|
354
|
-
res =
|
396
|
+
res = select_one(current_database_engine)
|
355
397
|
res['engine'] == 'Atomic' if res
|
356
398
|
end
|
357
399
|
|
358
400
|
def apply_cluster(sql)
|
359
|
-
|
401
|
+
if cluster
|
402
|
+
normalized_cluster_name = cluster.start_with?('{') ? "'#{cluster}'" : cluster
|
403
|
+
|
404
|
+
"#{sql} ON CLUSTER #{normalized_cluster_name}"
|
405
|
+
else
|
406
|
+
sql
|
407
|
+
end
|
360
408
|
end
|
361
409
|
|
362
410
|
def supports_insert_on_duplicate_skip?
|
@@ -393,6 +441,9 @@ module ActiveRecord
|
|
393
441
|
@connection.read_timeout = @connection_parameters[:read_timeout] if @connection_parameters[:read_timeout]
|
394
442
|
@connection.write_timeout = @connection_parameters[:write_timeout] if @connection_parameters[:write_timeout]
|
395
443
|
|
444
|
+
# Use clickhouse default keep_alive_timeout value of 10, rather than Net::HTTP's default of 2
|
445
|
+
@connection.keep_alive_timeout = @connection_parameters[:keep_alive_timeout] || 10
|
446
|
+
|
396
447
|
@connection
|
397
448
|
end
|
398
449
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'arel/visitors/to_sql'
|
2
|
+
|
3
|
+
module Arel
|
4
|
+
module Visitors
|
5
|
+
class Clickhouse < ::Arel::Visitors::ToSql
|
6
|
+
|
7
|
+
def aggregate(name, o, collector)
|
8
|
+
# replacing function name for materialized view
|
9
|
+
if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation&.is_view
|
10
|
+
super("#{name.downcase}Merge", o, collector)
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_Arel_Table o, collector
|
17
|
+
collector = super
|
18
|
+
collector << ' FINAL ' if o.final
|
19
|
+
collector
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_Arel_Nodes_SelectOptions(o, collector)
|
23
|
+
maybe_visit o.settings, super
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit_Arel_Nodes_Settings(o, collector)
|
27
|
+
return collector if o.expr.empty?
|
28
|
+
|
29
|
+
collector << "SETTINGS "
|
30
|
+
o.expr.each_with_index do |(key, value), i|
|
31
|
+
collector << ", " if i > 0
|
32
|
+
collector << key.to_s.gsub(/\W+/, "")
|
33
|
+
collector << " = "
|
34
|
+
collector << sanitize_as_setting_value(value)
|
35
|
+
end
|
36
|
+
collector
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit_Arel_Nodes_Using o, collector
|
40
|
+
collector << "USING "
|
41
|
+
visit o.expr, collector
|
42
|
+
collector
|
43
|
+
end
|
44
|
+
|
45
|
+
def sanitize_as_setting_value(value)
|
46
|
+
if value == :default
|
47
|
+
'DEFAULT'
|
48
|
+
else
|
49
|
+
quote(value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sanitize_as_setting_name(value)
|
54
|
+
return value if Arel::Nodes::SqlLiteral === value
|
55
|
+
@connection.sanitize_as_setting_name(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -10,16 +10,17 @@ module ClickhouseActiverecord
|
|
10
10
|
|
11
11
|
version_options = connection.internal_string_options_for_primary_key
|
12
12
|
table_options = {
|
13
|
-
id: false, options: 'ReplacingMergeTree(ver)
|
13
|
+
id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true
|
14
14
|
}
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
full_config = connection.instance_variable_get(:@full_config) || {}
|
16
|
+
|
17
|
+
if full_config[:distributed_service_tables]
|
18
|
+
table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)')
|
19
|
+
|
20
|
+
distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
|
20
21
|
end
|
21
22
|
|
22
|
-
connection.
|
23
|
+
connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
|
23
24
|
t.string :version, **version_options
|
24
25
|
t.column :active, 'Int8', null: false, default: '1'
|
25
26
|
t.datetime :ver, null: false, default: -> { 'now()' }
|
@@ -27,30 +28,43 @@ module ClickhouseActiverecord
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def all_versions
|
30
|
-
|
31
|
+
final.where(active: 1).order(:version).pluck(:version)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
36
|
class InternalMetadata < ::ActiveRecord::InternalMetadata
|
36
37
|
class << self
|
38
|
+
|
39
|
+
def []=(key, value)
|
40
|
+
row = final.find_by(key: key)
|
41
|
+
if row.nil? || row.value != value
|
42
|
+
create!(key: key, value: value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](key)
|
47
|
+
final.where(key: key).pluck(:value).first
|
48
|
+
end
|
49
|
+
|
37
50
|
def create_table
|
38
51
|
return if table_exists?
|
39
52
|
|
40
53
|
key_options = connection.internal_string_options_for_primary_key
|
41
54
|
table_options = {
|
42
55
|
id: false,
|
43
|
-
options: connection.adapter_name.downcase == 'clickhouse' ? '
|
56
|
+
options: connection.adapter_name.downcase == 'clickhouse' ? 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key' : '',
|
44
57
|
if_not_exists: true
|
45
58
|
}
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
full_config = connection.instance_variable_get(:@full_config) || {}
|
60
|
+
|
61
|
+
if full_config[:distributed_service_tables]
|
62
|
+
table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
|
63
|
+
|
64
|
+
distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
|
51
65
|
end
|
52
66
|
|
53
|
-
connection.
|
67
|
+
connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
|
54
68
|
t.string :key, **key_options
|
55
69
|
t.string :value
|
56
70
|
t.timestamps
|
@@ -120,5 +134,13 @@ module ClickhouseActiverecord
|
|
120
134
|
super
|
121
135
|
end
|
122
136
|
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def record_environment
|
141
|
+
return if down?
|
142
|
+
ClickhouseActiverecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
|
143
|
+
end
|
144
|
+
|
123
145
|
end
|
124
146
|
end
|
@@ -4,6 +4,12 @@ module ClickhouseActiverecord
|
|
4
4
|
require 'rails'
|
5
5
|
|
6
6
|
class Railtie < Rails::Railtie
|
7
|
+
initializer "clickhouse.load" do
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
ClickhouseActiverecord.load
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
rake_tasks { load 'tasks/clickhouse.rake' }
|
8
14
|
end
|
9
15
|
end
|
@@ -14,7 +14,7 @@ module ClickhouseActiverecord
|
|
14
14
|
connection.create_database @configuration["database"]
|
15
15
|
rescue ActiveRecord::StatementInvalid => e
|
16
16
|
if e.cause.to_s.include?('already exists')
|
17
|
-
raise ActiveRecord::
|
17
|
+
raise ActiveRecord::DatabaseAlreadyExists
|
18
18
|
else
|
19
19
|
raise
|
20
20
|
end
|
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'active_record/connection_adapters/clickhouse_adapter'
|
4
4
|
|
5
|
+
require 'core_extensions/active_record/relation'
|
6
|
+
|
7
|
+
require 'core_extensions/arel/nodes/select_statement'
|
8
|
+
require 'core_extensions/arel/select_manager'
|
9
|
+
require 'core_extensions/arel/table'
|
10
|
+
|
5
11
|
require_relative '../core_extensions/active_record/migration/command_recorder'
|
6
12
|
ActiveRecord::Migration::CommandRecorder.include CoreExtensions::ActiveRecord::Migration::CommandRecorder
|
7
13
|
|
@@ -14,5 +20,11 @@ if defined?(Rails::Railtie)
|
|
14
20
|
end
|
15
21
|
|
16
22
|
module ClickhouseActiverecord
|
23
|
+
def self.load
|
24
|
+
ActiveRecord::Relation.prepend(CoreExtensions::ActiveRecord::Relation)
|
17
25
|
|
26
|
+
Arel::Nodes::SelectStatement.prepend(CoreExtensions::Arel::Nodes::SelectStatement)
|
27
|
+
Arel::SelectManager.prepend(CoreExtensions::Arel::SelectManager)
|
28
|
+
Arel::Table.prepend(CoreExtensions::Arel::Table)
|
29
|
+
end
|
18
30
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CoreExtensions
|
2
|
+
module ActiveRecord
|
3
|
+
module Relation
|
4
|
+
def reverse_order!
|
5
|
+
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
|
6
|
+
|
7
|
+
orders = order_values.uniq.reject(&:blank?)
|
8
|
+
return super unless orders.empty? && !primary_key
|
9
|
+
|
10
|
+
self.order_values = (column_names & %w[date created_at]).map { |c| arel_table[c].desc }
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Hash] opts
|
15
|
+
def settings(**opts)
|
16
|
+
check_command('SETTINGS')
|
17
|
+
@values[:settings] = (@values[:settings] || {}).merge opts
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [Boolean] final
|
22
|
+
def final(final = true)
|
23
|
+
check_command('FINAL')
|
24
|
+
@table = @table.dup
|
25
|
+
@table.final = final
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_command(cmd)
|
32
|
+
raise ::ActiveRecord::ActiveRecordError, cmd + ' is a ClickHouse specific query clause' unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_arel(aliases = nil)
|
36
|
+
arel = super
|
37
|
+
|
38
|
+
arel.settings(@values[:settings]) if @values[:settings].present?
|
39
|
+
|
40
|
+
arel
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CoreExtensions
|
2
|
+
module Arel # :nodoc: all
|
3
|
+
module Nodes
|
4
|
+
module SelectStatement
|
5
|
+
attr_accessor :settings
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
@settings = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def eql?(other)
|
13
|
+
super && settings == other.settings
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CoreExtensions
|
2
|
+
module Arel
|
3
|
+
module SelectManager
|
4
|
+
|
5
|
+
# @param [Hash] values
|
6
|
+
def settings(values)
|
7
|
+
@ast.settings = ::Arel::Nodes::Settings.new(values)
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def using(*exprs)
|
12
|
+
@ctx.source.right.last.right = ::Arel::Nodes::Using.new(::Arel.sql(exprs.join(',')))
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
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.6.1
|
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: 2023-11-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -31,6 +31,9 @@ dependencies:
|
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '5.2'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '7'
|
34
37
|
type: :runtime
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -38,20 +41,9 @@ dependencies:
|
|
38
41
|
- - ">="
|
39
42
|
- !ruby/object:Gem::Version
|
40
43
|
version: '5.2'
|
41
|
-
-
|
42
|
-
name: bundler
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.15'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
44
|
+
- - "<"
|
53
45
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
46
|
+
version: '7'
|
55
47
|
- !ruby/object:Gem::Dependency
|
56
48
|
name: rake
|
57
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,22 +113,27 @@ files:
|
|
121
113
|
- lib/active_record/connection_adapters/clickhouse/schema_definitions.rb
|
122
114
|
- lib/active_record/connection_adapters/clickhouse/schema_statements.rb
|
123
115
|
- lib/active_record/connection_adapters/clickhouse_adapter.rb
|
116
|
+
- lib/arel/nodes/settings.rb
|
117
|
+
- lib/arel/nodes/using.rb
|
118
|
+
- lib/arel/visitors/clickhouse.rb
|
124
119
|
- lib/clickhouse-activerecord.rb
|
125
|
-
- lib/clickhouse-activerecord/arel/table.rb
|
126
|
-
- lib/clickhouse-activerecord/arel/visitors/to_sql.rb
|
127
120
|
- lib/clickhouse-activerecord/migration.rb
|
128
121
|
- lib/clickhouse-activerecord/railtie.rb
|
129
122
|
- lib/clickhouse-activerecord/schema.rb
|
130
123
|
- lib/clickhouse-activerecord/schema_dumper.rb
|
131
124
|
- lib/clickhouse-activerecord/tasks.rb
|
132
125
|
- lib/clickhouse-activerecord/version.rb
|
126
|
+
- lib/core_extensions/active_record/relation.rb
|
127
|
+
- lib/core_extensions/arel/nodes/select_statement.rb
|
128
|
+
- lib/core_extensions/arel/select_manager.rb
|
129
|
+
- lib/core_extensions/arel/table.rb
|
133
130
|
- lib/generators/clickhouse_migration_generator.rb
|
134
131
|
- lib/tasks/clickhouse.rake
|
135
132
|
homepage: https://github.com/pnixx/clickhouse-activerecord
|
136
133
|
licenses:
|
137
134
|
- MIT
|
138
135
|
metadata: {}
|
139
|
-
post_install_message:
|
136
|
+
post_install_message:
|
140
137
|
rdoc_options: []
|
141
138
|
require_paths:
|
142
139
|
- lib
|
@@ -151,8 +148,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
148
|
- !ruby/object:Gem::Version
|
152
149
|
version: '0'
|
153
150
|
requirements: []
|
154
|
-
rubygems_version: 3.
|
155
|
-
signing_key:
|
151
|
+
rubygems_version: 3.1.6
|
152
|
+
signing_key:
|
156
153
|
specification_version: 4
|
157
154
|
summary: ClickHouse ActiveRecord
|
158
155
|
test_files: []
|
@@ -1,20 +0,0 @@
|
|
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
|
-
# replacing function name for materialized view
|
10
|
-
if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation&.is_view
|
11
|
-
super("#{name.downcase}Merge", o, collector)
|
12
|
-
else
|
13
|
-
super
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|