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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79b43575e5eef93daa11e1a2aca2984f696cbef59021f55d1470b2dc0a5d1e3d
4
- data.tar.gz: b79cccb85d8a07fa4dc533d7b7e9042eeaa2afead26214c075b3131e1843394b
3
+ metadata.gz: 74728f97bde396aae27436dd6b7ec8d57ec172e818122f9094d994583c92781f
4
+ data.tar.gz: 64e08f74a95d755338b489b32f2aade49685b6b604a3d02d2f232ca9f24b7b23
5
5
  SHA512:
6
- metadata.gz: 840b01cc1d5b88eee5e031fada23cfd35f7a8d948a35a767b2f5baf1596af665f28c56778e699960511004367b16ee3130be1f5bd2ee713a9a0d5053a9b58a37
7
- data.tar.gz: 4e613a19c51b362a05634da06d9528809ed09f01ebec4eac9984656156e9b877bdc4ce321e24bea63ae4e49d85609fa9dda781db60159e60b6d6ee06e8a99a3e
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 20.9 LTS.
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 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
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.to_time.strftime('%Y-%m-%d %H:%M:%S')
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}.#{options.to.sub('.', '')} "
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::Base.connection_db_config.database
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('SHOW TABLES', name)
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
- execute insert_versions_sql(inserting)
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.presence && JSON.parse(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
- raise ActiveRecord::ActiveRecordError,
111
- "Response code: #{res.code}:\n#{res.body}"
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 'clickhouse-activerecord/arel/visitors/to_sql'
4
- require 'clickhouse-activerecord/arel/table'
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
- module ClassMethods
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
- @arel_table ||= ClickhouseActiverecord::Arel::Table.new(table_name, type_caster: type_caster)
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
- end
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: 'UInt8' },
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
- ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
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
- register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
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
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
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
- value.to_s(:db)
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
- execute schema_creation.accept td
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
- execute schema_creation.accept td
285
- end
310
+ do_execute(schema_creation.accept(td), format: nil)
286
311
 
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
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
- 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) }
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 "ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}"
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 = ActiveRecord::Base.connection.select_one(current_database_engine)
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
- cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
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,11 @@
1
+ module Arel # :nodoc: all
2
+ module Nodes
3
+ class Settings < Arel::Nodes::Unary
4
+ def initialize(expr)
5
+ raise ArgumentError, 'Settings must be a Hash' unless expr.is_a?(Hash)
6
+
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Arel # :nodoc: all
2
+ module Nodes
3
+ class Using < Arel::Nodes::Unary
4
+ end
5
+ end
6
+ end
@@ -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) PARTITION BY version ORDER BY (version)', if_not_exists: true
13
+ id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true
14
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'
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.public_send(table_creation_method, table_name, **table_options) do |t|
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
- from("#{table_name} FINAL").where(active: 1).order(:version).pluck(:version)
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' ? 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)' : '',
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
- 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'
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.public_send(table_creation_method, table_name, **table_options) do |t|
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::Tasks::DatabaseAlreadyExists
17
+ raise ActiveRecord::DatabaseAlreadyExists
18
18
  else
19
19
  raise
20
20
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '0.5.7'
2
+ VERSION = '0.6.1'
3
3
  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
@@ -1,6 +1,8 @@
1
- module ClickhouseActiverecord
1
+ module CoreExtensions
2
2
  module Arel
3
- class Table < ::Arel::Table
3
+ module Table
4
+ attr_accessor :final
5
+
4
6
  def is_view
5
7
  type_caster.is_view
6
8
  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.5.7
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: 2021-10-25 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
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: '1.15'
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.0.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