clickhouse-activerecord 1.0.4 → 1.0.6

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.
@@ -94,6 +94,23 @@ module ActiveRecord
94
94
  args.each { |name| column(name, kind, **options.except(:limit)) }
95
95
  end
96
96
  end
97
+
98
+ class IndexDefinition
99
+ attr_reader :table, :name, :expression, :type, :granularity, :first, :after, :if_exists, :if_not_exists
100
+
101
+ def initialize(table, name, expression, type, granularity, first:, after:, if_exists:, if_not_exists:)
102
+ @table = table
103
+ @name = name
104
+ @expression = expression
105
+ @type = type
106
+ @granularity = granularity
107
+ @first = first
108
+ @after = after
109
+ @if_exists = if_exists
110
+ @if_not_exists = if_not_exists
111
+ end
112
+
113
+ end
97
114
  end
98
115
  end
99
116
  end
@@ -6,6 +6,8 @@ module ActiveRecord
6
6
  module ConnectionAdapters
7
7
  module Clickhouse
8
8
  module SchemaStatements
9
+ DEFAULT_RESPONSE_FORMAT = 'JSONCompactEachRowWithNamesAndTypes'.freeze
10
+
9
11
  def execute(sql, name = nil, settings: {})
10
12
  do_execute(sql, name, settings: settings)
11
13
  end
@@ -33,13 +35,20 @@ module ActiveRecord
33
35
  # @link https://clickhouse.com/docs/en/sql-reference/statements/alter/update
34
36
  def exec_update(_sql, _name = nil, _binds = [])
35
37
  do_execute(_sql, _name, format: nil)
36
- true
38
+ 0
37
39
  end
38
40
 
39
41
  # @link https://clickhouse.com/docs/en/sql-reference/statements/delete
40
42
  def exec_delete(_sql, _name = nil, _binds = [])
41
- do_execute(_sql, _name, format: nil)
42
- true
43
+ log(_sql, "#{adapter_name} #{_name}") do
44
+ res = request(_sql)
45
+ begin
46
+ data = JSON.parse(res.header['x-clickhouse-summary'])
47
+ data['result_rows'].to_i
48
+ rescue JSONError
49
+ 0
50
+ end
51
+ end
43
52
  end
44
53
 
45
54
  def tables(name = nil)
@@ -48,6 +57,16 @@ module ActiveRecord
48
57
  result['data'].flatten
49
58
  end
50
59
 
60
+ def functions
61
+ result = do_system_execute("SELECT name FROM system.functions WHERE origin = 'SQLUserDefined'")
62
+ return [] if result.nil?
63
+ result['data'].flatten
64
+ end
65
+
66
+ def show_create_function(function)
67
+ do_execute("SELECT create_query FROM system.functions WHERE origin = 'SQLUserDefined' AND name = '#{function}'", format: nil)
68
+ end
69
+
51
70
  def table_options(table)
52
71
  sql = show_create_table(table)
53
72
  { options: sql.gsub(/^(?:.*?)(?:ENGINE = (.*?))?( AS SELECT .*?)?$/, '\\1').presence, as: sql.match(/^CREATE (?:.*?) AS (SELECT .*?)$/).try(:[], 1) }.compact
@@ -58,25 +77,29 @@ module ActiveRecord
58
77
  []
59
78
  end
60
79
 
80
+ def add_index_options(table_name, expression, **options)
81
+ options.assert_valid_keys(:name, :type, :granularity, :first, :after, :if_not_exists, :if_exists)
82
+
83
+ validate_index_length!(table_name, options[:name])
84
+
85
+ IndexDefinition.new(table_name, options[:name], expression, options[:type], options[:granularity], first: options[:first], after: options[:after], if_not_exists: options[:if_not_exists], if_exists: options[:if_exists])
86
+ end
87
+
61
88
  def data_sources
62
89
  tables
63
90
  end
64
91
 
65
92
  def do_system_execute(sql, name = nil)
66
93
  log_with_debug(sql, "#{adapter_name} #{name}") do
67
- res = @connection.post("/?#{@connection_config.to_param}", "#{sql} FORMAT JSONCompact", 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
68
-
69
- process_response(res)
94
+ res = request(sql, DEFAULT_RESPONSE_FORMAT)
95
+ process_response(res, DEFAULT_RESPONSE_FORMAT, sql)
70
96
  end
71
97
  end
72
98
 
73
- def do_execute(sql, name = nil, format: 'JSONCompact', settings: {})
99
+ def do_execute(sql, name = nil, format: DEFAULT_RESPONSE_FORMAT, settings: {})
74
100
  log(sql, "#{adapter_name} #{name}") do
75
- formatted_sql = apply_format(sql, format)
76
- request_params = @connection_config || {}
77
- res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
78
-
79
- process_response(res)
101
+ res = request(sql, format, settings)
102
+ process_response(res, format, sql)
80
103
  end
81
104
  end
82
105
 
@@ -96,7 +119,7 @@ module ActiveRecord
96
119
  if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
97
120
  raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
98
121
  end
99
- do_execute(insert_versions_sql(inserting), nil, settings: {max_partitions_per_insert_block: [100, inserting.size].max})
122
+ do_execute(insert_versions_sql(inserting), nil, format: nil, settings: {max_partitions_per_insert_block: [100, inserting.size].max})
100
123
  end
101
124
  end
102
125
 
@@ -112,17 +135,28 @@ module ActiveRecord
112
135
 
113
136
  private
114
137
 
138
+ # Make HTTP request to ClickHouse server
139
+ # @param [String] sql
140
+ # @param [String, nil] format
141
+ # @param [Hash] settings
142
+ # @return [Net::HTTPResponse]
143
+ def request(sql, format = nil, settings = {})
144
+ formatted_sql = apply_format(sql, format)
145
+ request_params = @connection_config || {}
146
+ @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
147
+ end
148
+
115
149
  def apply_format(sql, format)
116
150
  format ? "#{sql} FORMAT #{format}" : sql
117
151
  end
118
152
 
119
- def process_response(res)
153
+ def process_response(res, format, sql = nil)
120
154
  case res.code.to_i
121
155
  when 200
122
156
  if res.body.to_s.include?("DB::Exception")
123
- raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
157
+ raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}#{sql ? "\nQuery: #{sql}" : ''}"
124
158
  else
125
- res.body.presence && JSON.parse(res.body)
159
+ format_body_response(res.body, format)
126
160
  end
127
161
  else
128
162
  case res.body
@@ -168,8 +202,7 @@ module ActiveRecord
168
202
 
169
203
  return data unless data.empty?
170
204
 
171
- raise ActiveRecord::StatementInvalid,
172
- "Could not find table '#{table_name}'"
205
+ raise ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'"
173
206
  end
174
207
  alias column_definitions table_structure
175
208
 
@@ -203,6 +236,44 @@ module ActiveRecord
203
236
  def has_default_function?(default_value, default) # :nodoc:
204
237
  !default_value && (%r{\w+\(.*\)} === default)
205
238
  end
239
+
240
+ def format_body_response(body, format)
241
+ return body if body.blank?
242
+
243
+ case format
244
+ when 'JSONCompact'
245
+ format_from_json_compact(body)
246
+ when 'JSONCompactEachRowWithNamesAndTypes'
247
+ format_from_json_compact_each_row_with_names_and_types(body)
248
+ else
249
+ body
250
+ end
251
+ end
252
+
253
+ def format_from_json_compact(body)
254
+ parse_json_payload(body)
255
+ end
256
+
257
+ def format_from_json_compact_each_row_with_names_and_types(body)
258
+ rows = body.split("\n").map { |row| parse_json_payload(row) }
259
+ names, types, *data = rows
260
+
261
+ meta = names.zip(types).map do |name, type|
262
+ {
263
+ 'name' => name,
264
+ 'type' => type
265
+ }
266
+ end
267
+
268
+ {
269
+ 'meta' => meta,
270
+ 'data' => data
271
+ }
272
+ end
273
+
274
+ def parse_json_payload(payload)
275
+ JSON.parse(payload, decimal_class: BigDecimal)
276
+ end
206
277
  end
207
278
  end
208
279
  end
@@ -73,10 +73,11 @@ module ActiveRecord
73
73
  def is_view=(value)
74
74
  @is_view = value
75
75
  end
76
- #
77
- # def arel_table # :nodoc:
78
- # @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
79
- # end
76
+
77
+ def _delete_record(constraints)
78
+ raise ActiveRecord::ActiveRecordError.new('Deleting a row is not possible without a primary key') unless self.primary_key
79
+ super
80
+ end
80
81
  end
81
82
  end
82
83
 
@@ -148,6 +149,10 @@ module ActiveRecord
148
149
  !native_database_types[type].nil?
149
150
  end
150
151
 
152
+ def supports_indexes_in_create?
153
+ true
154
+ end
155
+
151
156
  class << self
152
157
  def extract_limit(sql_type) # :nodoc:
153
158
  case sql_type
@@ -267,7 +272,7 @@ module ActiveRecord
267
272
  sql = apply_cluster "CREATE DATABASE #{quote_table_name(name)}"
268
273
  log_with_debug(sql, adapter_name) do
269
274
  res = @connection.post("/?#{@connection_config.except(:database).to_param}", sql)
270
- process_response(res)
275
+ process_response(res, DEFAULT_RESPONSE_FORMAT)
271
276
  end
272
277
  end
273
278
 
@@ -307,12 +312,23 @@ module ActiveRecord
307
312
  end
308
313
  end
309
314
 
315
+ def create_function(name, body)
316
+ fd = "CREATE FUNCTION #{apply_cluster(quote_table_name(name))} AS #{body}"
317
+ do_execute(fd, format: nil)
318
+ end
319
+
310
320
  # Drops a ClickHouse database.
311
321
  def drop_database(name) #:nodoc:
312
322
  sql = apply_cluster "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
313
323
  log_with_debug(sql, adapter_name) do
314
324
  res = @connection.post("/?#{@connection_config.except(:database).to_param}", sql)
315
- process_response(res)
325
+ process_response(res, DEFAULT_RESPONSE_FORMAT)
326
+ end
327
+ end
328
+
329
+ def drop_functions
330
+ functions.each do |function|
331
+ drop_function(function)
316
332
  end
317
333
  end
318
334
 
@@ -335,6 +351,16 @@ module ActiveRecord
335
351
  end
336
352
  end
337
353
 
354
+ def drop_function(name, options = {})
355
+ query = "DROP FUNCTION"
356
+ query = "#{query} IF EXISTS " if options[:if_exists]
357
+ query = "#{query} #{quote_table_name(name)}"
358
+ query = apply_cluster(query)
359
+ query = "#{query} SYNC" if options[:sync]
360
+
361
+ do_execute(query, format: nil)
362
+ end
363
+
338
364
  def add_column(table_name, column_name, type, **options)
339
365
  return if options[:if_not_exists] == true && column_exists?(table_name, column_name, type)
340
366
 
@@ -349,8 +375,8 @@ module ActiveRecord
349
375
  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})
350
376
  end
351
377
 
352
- def change_column(table_name, column_name, type, options = {})
353
- 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})
378
+ def change_column(table_name, column_name, type, **options)
379
+ 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})
354
380
  raise "Error parse json response: #{result}" if result.presence && !result.is_a?(Hash)
355
381
  end
356
382
 
@@ -364,6 +390,39 @@ module ActiveRecord
364
390
  change_column table_name, column_name, nil, {default: default}.compact
365
391
  end
366
392
 
393
+ # Adds index description to tables metadata
394
+ # @link https://clickhouse.com/docs/en/sql-reference/statements/alter/skipping-index
395
+ def add_index(table_name, expression, **options)
396
+ index = add_index_options(apply_cluster(table_name), expression, **options)
397
+ execute schema_creation.accept(CreateIndexDefinition.new(index))
398
+ end
399
+
400
+ # Removes index description from tables metadata and deletes index files from disk
401
+ def remove_index(table_name, name)
402
+ query = apply_cluster("ALTER TABLE #{quote_table_name(table_name)}")
403
+ execute "#{query} DROP INDEX #{quote_column_name(name)}"
404
+ end
405
+
406
+ # Rebuilds the secondary index name for the specified partition_name
407
+ def rebuild_index(table_name, name, if_exists: false, partition: nil)
408
+ query = [apply_cluster("ALTER TABLE #{quote_table_name(table_name)}")]
409
+ query << 'MATERIALIZE INDEX'
410
+ query << 'IF EXISTS' if if_exists
411
+ query << quote_column_name(name)
412
+ query << "IN PARTITION #{quote_column_name(partition)}" if partition
413
+ execute query.join(' ')
414
+ end
415
+
416
+ # Deletes the secondary index files from disk without removing description
417
+ def clear_index(table_name, name, if_exists: false, partition: nil)
418
+ query = [apply_cluster("ALTER TABLE #{quote_table_name(table_name)}")]
419
+ query << 'CLEAR INDEX'
420
+ query << 'IF EXISTS' if if_exists
421
+ query << quote_column_name(name)
422
+ query << "IN PARTITION #{quote_column_name(partition)}" if partition
423
+ execute query.join(' ')
424
+ end
425
+
367
426
  def cluster
368
427
  @config[:cluster_name]
369
428
  end
@@ -419,9 +478,9 @@ module ActiveRecord
419
478
  result
420
479
  end
421
480
 
422
- def change_column_for_alter(table_name, column_name, type, options = {})
481
+ def change_column_for_alter(table_name, column_name, type, **options)
423
482
  td = create_table_definition(table_name)
424
- cd = td.new_column_definition(column_name, type, options)
483
+ cd = td.new_column_definition(column_name, type, **options)
425
484
  schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))
426
485
  end
427
486
 
@@ -13,6 +13,13 @@ module Arel
13
13
  end
14
14
  end
15
15
 
16
+ # https://clickhouse.com/docs/en/sql-reference/statements/delete
17
+ # DELETE and UPDATE in ClickHouse working only without table name
18
+ def visit_Arel_Attributes_Attribute(o, collector)
19
+ collector << quote_table_name(o.relation.table_alias || o.relation.name) << '.' unless collector.value.start_with?('DELETE FROM ') || collector.value.include?(' UPDATE ')
20
+ collector << quote_column_name(o.name)
21
+ end
22
+
16
23
  def visit_Arel_Nodes_SelectOptions(o, collector)
17
24
  maybe_visit o.settings, super
18
25
  end
@@ -34,8 +34,12 @@ HEADER
34
34
  end
35
35
 
36
36
  def tables(stream)
37
- sorted_tables = @connection.tables.sort {|a,b| @connection.show_create_table(a).match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : a <=> b }
37
+ functions = @connection.functions
38
+ functions.each do |function|
39
+ function(function, stream)
40
+ end
38
41
 
42
+ sorted_tables = @connection.tables.sort {|a,b| @connection.show_create_table(a).match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : a <=> b }
39
43
  sorted_tables.each do |table_name|
40
44
  table(table_name, stream) unless ignored?(table_name)
41
45
  end
@@ -104,7 +108,13 @@ HEADER
104
108
  end
105
109
  end
106
110
 
107
- indexes_in_create(table, tbl)
111
+ indexes = sql.scan(/INDEX \S+ \S+ TYPE .*? GRANULARITY \d+/)
112
+ if indexes.any?
113
+ tbl.puts ''
114
+ indexes.flatten.map!(&:strip).each do |index|
115
+ tbl.puts " t.index #{index_parts(index).join(', ')}"
116
+ end
117
+ end
108
118
 
109
119
  tbl.puts " end"
110
120
  tbl.puts
@@ -119,6 +129,13 @@ HEADER
119
129
  end
120
130
  end
121
131
 
132
+ def function(function, stream)
133
+ stream.puts " # FUNCTION: #{function}"
134
+ sql = @connection.show_create_function(function)
135
+ stream.puts " # SQL: #{sql}" if sql
136
+ stream.puts " create_function \"#{function}\", \"#{sql.gsub(/^CREATE FUNCTION (.*?) AS/, '').strip}\"" if sql
137
+ end
138
+
122
139
  def format_options(options)
123
140
  if options && options[:options]
124
141
  options[:options] = options[:options].gsub(/^Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "\\1(\\2)")
@@ -154,5 +171,16 @@ HEADER
154
171
  spec[:array] = schema_array(column)
155
172
  spec.merge(super).compact
156
173
  end
174
+
175
+ def index_parts(index)
176
+ idx = index.match(/^INDEX (?<name>\S+) (?<expr>.*?) TYPE (?<type>.*?) GRANULARITY (?<granularity>\d+)$/)
177
+ index_parts = [
178
+ format_index_parts(idx['expr']),
179
+ "name: #{format_index_parts(idx['name'])}",
180
+ "type: #{format_index_parts(idx['type'])}",
181
+ ]
182
+ index_parts << "granularity: #{idx['granularity']}" if idx['granularity']
183
+ index_parts
184
+ end
157
185
  end
158
186
  end
@@ -5,12 +5,12 @@ module ClickhouseActiverecord
5
5
  delegate :connection, :establish_connection, to: ActiveRecord::Base
6
6
 
7
7
  def initialize(configuration)
8
- @configuration = configuration.with_indifferent_access
8
+ @configuration = configuration
9
9
  end
10
10
 
11
11
  def create
12
12
  establish_master_connection
13
- connection.create_database @configuration['database']
13
+ connection.create_database @configuration.database
14
14
  rescue ActiveRecord::StatementInvalid => e
15
15
  if e.cause.to_s.include?('already exists')
16
16
  raise ActiveRecord::DatabaseAlreadyExists
@@ -21,7 +21,7 @@ module ClickhouseActiverecord
21
21
 
22
22
  def drop
23
23
  establish_master_connection
24
- connection.drop_database @configuration['database']
24
+ connection.drop_database @configuration.database
25
25
  end
26
26
 
27
27
  def purge
@@ -31,12 +31,28 @@ module ClickhouseActiverecord
31
31
  end
32
32
 
33
33
  def structure_dump(*args)
34
- tables = connection.execute("SHOW TABLES FROM #{@configuration['database']}")['data'].flatten
34
+ establish_master_connection
35
+
36
+ # get all tables
37
+ tables = connection.execute("SHOW TABLES FROM #{@configuration.database} WHERE name NOT LIKE '.inner_id.%'")['data'].flatten.map do |table|
38
+ next if %w[schema_migrations ar_internal_metadata].include?(table)
39
+ connection.show_create_table(table).gsub("#{@configuration.database}.", '')
40
+ end.compact
41
+
42
+ # sort view to last
43
+ tables.sort_by! {|table| table.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : 0}
35
44
 
45
+ # get all functions
46
+ functions = connection.execute("SELECT create_query FROM system.functions WHERE origin = 'SQLUserDefined'")['data'].flatten
47
+
48
+ # put to file
36
49
  File.open(args.first, 'w:utf-8') do |file|
50
+ functions.each do |function|
51
+ file.puts function + ";\n\n"
52
+ end
53
+
37
54
  tables.each do |table|
38
- next if table.match(/\.inner/)
39
- file.puts connection.execute("SHOW CREATE TABLE #{table}")['data'].try(:first).try(:first).gsub("#{@configuration['database']}.", '') + ";\n\n"
55
+ file.puts table + ";\n\n"
40
56
  end
41
57
  end
42
58
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '1.0.4'
2
+ VERSION = '1.0.6'
3
3
  end
@@ -5,15 +5,12 @@ require 'active_record/connection_adapters/clickhouse_adapter'
5
5
  require 'core_extensions/active_record/internal_metadata'
6
6
  require 'core_extensions/active_record/relation'
7
7
  require 'core_extensions/active_record/schema_migration'
8
-
8
+ require 'core_extensions/active_record/migration/command_recorder'
9
9
  require 'core_extensions/arel/nodes/select_core'
10
10
  require 'core_extensions/arel/nodes/select_statement'
11
11
  require 'core_extensions/arel/select_manager'
12
12
  require 'core_extensions/arel/table'
13
13
 
14
- require_relative '../core_extensions/active_record/migration/command_recorder'
15
- ActiveRecord::Migration::CommandRecorder.include CoreExtensions::ActiveRecord::Migration::CommandRecorder
16
-
17
14
  if defined?(Rails::Railtie)
18
15
  require 'clickhouse-activerecord/railtie'
19
16
  require 'clickhouse-activerecord/schema'
@@ -24,9 +21,10 @@ end
24
21
 
25
22
  module ClickhouseActiverecord
26
23
  def self.load
27
- ActiveRecord::InternalMetadata.singleton_class.prepend(CoreExtensions::ActiveRecord::InternalMetadata::ClassMethods)
24
+ ActiveRecord::InternalMetadata.prepend(CoreExtensions::ActiveRecord::InternalMetadata)
25
+ ActiveRecord::Migration::CommandRecorder.include(CoreExtensions::ActiveRecord::Migration::CommandRecorder)
28
26
  ActiveRecord::Relation.prepend(CoreExtensions::ActiveRecord::Relation)
29
- ActiveRecord::SchemaMigration.singleton_class.prepend(CoreExtensions::ActiveRecord::SchemaMigration::ClassMethods)
27
+ ActiveRecord::SchemaMigration.prepend(CoreExtensions::ActiveRecord::SchemaMigration)
30
28
 
31
29
  Arel::Nodes::SelectCore.prepend(CoreExtensions::Arel::Nodes::SelectCore)
32
30
  Arel::Nodes::SelectStatement.prepend(CoreExtensions::Arel::Nodes::SelectStatement)
@@ -1,46 +1,54 @@
1
1
  module CoreExtensions
2
2
  module ActiveRecord
3
3
  module InternalMetadata
4
- module ClassMethods
5
4
 
6
- def []=(key, value)
7
- row = final.find_by(key: key)
8
- if row.nil? || row.value != value
9
- create!(key: key, value: value)
10
- end
11
- end
5
+ def create_table
6
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
7
+ return if table_exists? || !enabled?
8
+
9
+ key_options = connection.internal_string_options_for_primary_key
10
+ table_options = {
11
+ id: false,
12
+ options: 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key',
13
+ if_not_exists: true
14
+ }
15
+ full_config = connection.instance_variable_get(:@config) || {}
16
+
17
+ if full_config[:distributed_service_tables]
18
+ table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
12
19
 
13
- def [](key)
14
- final.where(key: key).pluck(:value).first
20
+ distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
21
+ else
22
+ distributed_suffix = ''
15
23
  end
16
24
 
17
- def create_table
18
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
19
- return if table_exists? || !enabled?
20
-
21
- key_options = connection.internal_string_options_for_primary_key
22
- table_options = {
23
- id: false,
24
- options: connection.adapter_name.downcase == 'clickhouse' ? 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key' : '',
25
- if_not_exists: true
26
- }
27
- full_config = connection.instance_variable_get(:@config) || {}
28
-
29
- if full_config[:distributed_service_tables]
30
- table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
31
-
32
- distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
33
- else
34
- distributed_suffix = ''
35
- end
36
-
37
- connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
38
- t.string :key, **key_options
39
- t.string :value
40
- t.timestamps
41
- end
25
+ connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
26
+ t.string :key, **key_options
27
+ t.string :value
28
+ t.timestamps
42
29
  end
43
30
  end
31
+
32
+ private
33
+
34
+ def update_entry(key, new_value)
35
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
36
+
37
+ create_entry(key, new_value)
38
+ end
39
+
40
+ def select_entry(key)
41
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
42
+
43
+ sm = ::Arel::SelectManager.new(arel_table)
44
+ sm.final! if connection.table_options(table_name)[:options] =~ /^ReplacingMergeTree/
45
+ sm.project(::Arel.star)
46
+ sm.where(arel_table[primary_key].eq(::Arel::Nodes::BindParam.new(key)))
47
+ sm.order(arel_table[primary_key].asc)
48
+ sm.limit = 1
49
+
50
+ connection.select_one(sm, "#{self.class} Load")
51
+ end
44
52
  end
45
53
  end
46
54
  end