clickhouse-activerecord 0.5.7 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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