clickhouse-activerecord 1.0.5 → 1.0.6

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: 2217c1539b22307398fbc9c01bb8b521300ca23a7d03c2d32a56a9abdd3c5546
4
- data.tar.gz: 8aa60f1a78db38df4120b0b405ddaf9466da70e68bf382f58945b29d5e130b7b
3
+ metadata.gz: 01f256bfb493f9961b3c0268d7f71009468b7b28c00deba7901d064f74b91c5c
4
+ data.tar.gz: 13b366b2264636610baa3bde60b9c82930c80b13201ac41724d37d4430fa60dd
5
5
  SHA512:
6
- metadata.gz: d1e48e302475eff9ca738b35fc56b58a7a73f7d6b6cf5ae6632a7ee95fbc9432734ba18c53f7756d833ebd3748368fd73f7c29e04a0972f3141d00b5acd262cd
7
- data.tar.gz: acedbb7298cd044769a5a1deaaae44672bdbf1affca5d2218e8b7209731df505434ed4f69bb23881e0d4f231e9e6699ac621ab0b4ac19f86475dd6c6515b3e18
6
+ metadata.gz: 75bafd27ee374a30f8a519937591254f2f316e3bf477b6298da709652064853cc491f454950fefc1558e66b170c4773b167d1988aaa9dd38b583d79ba60b3c4d
7
+ data.tar.gz: 23709bae8fdf85dda4be2f70d41016be9e8cc10ba503ffc73dcc42e69ca0573f792f4635fa7d1b476b74e394ba1dd2f4fc6774c42d96821c13b55f798094666e
@@ -15,6 +15,9 @@ services:
15
15
  volumes:
16
16
  - './clickhouse/cluster/server1_config.xml:/etc/clickhouse-server/config.xml'
17
17
  - './clickhouse/users.xml:/etc/clickhouse-server/users.xml'
18
+ healthcheck:
19
+ test: bash -c "exec 6<> /dev/tcp/localhost/8123"
20
+ interval: 5s
18
21
 
19
22
  clickhouse2:
20
23
  image: 'clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-23.11-alpine}'
@@ -29,6 +32,9 @@ services:
29
32
  volumes:
30
33
  - './clickhouse/cluster/server2_config.xml:/etc/clickhouse-server/config.xml'
31
34
  - './clickhouse/users.xml:/etc/clickhouse-server/users.xml'
35
+ healthcheck:
36
+ test: bash -c "exec 6<> /dev/tcp/localhost/8123"
37
+ interval: 5s
32
38
 
33
39
  # Using Nginx as a cluster entrypoint and a round-robin load balancer for HTTP requests
34
40
  nginx:
@@ -39,3 +45,8 @@ services:
39
45
  volumes:
40
46
  - './nginx/local.conf:/etc/nginx/conf.d/local.conf'
41
47
  container_name: clickhouse-activerecord-nginx
48
+ depends_on:
49
+ clickhouse1:
50
+ condition: service_healthy
51
+ clickhouse2:
52
+ condition: service_healthy
@@ -12,3 +12,6 @@ services:
12
12
  volumes:
13
13
  - './clickhouse/single/config.xml:/etc/clickhouse-server/config.xml'
14
14
  - './clickhouse/users.xml:/etc/clickhouse-server/users.xml'
15
+ healthcheck:
16
+ test: bash -c "exec 6<> /dev/tcp/localhost/8123"
17
+ interval: 5s
data/README.md CHANGED
@@ -203,7 +203,7 @@ false`. The default integer is `UInt32`
203
203
  Example:
204
204
 
205
205
  ``` ruby
206
- class CreateDataItems < ActiveRecord::Migration
206
+ class CreateDataItems < ActiveRecord::Migration[7.1]
207
207
  def change
208
208
  create_table "data_items", id: false, options: "VersionedCollapsingMergeTree(sign, version) PARTITION BY toYYYYMM(day) ORDER BY category", force: :cascade do |t|
209
209
  t.date "day", null: false
@@ -212,9 +212,32 @@ class CreateDataItems < ActiveRecord::Migration
212
212
  t.integer "sign", limit: 1, unsigned: false, default: -> { "CAST(1, 'Int8')" }, null: false
213
213
  t.integer "version", limit: 8, default: -> { "CAST(toUnixTimestamp(now()), 'UInt64')" }, null: false
214
214
  end
215
+
216
+ create_table "with_index", id: false, options: 'MergeTree PARTITION BY toYYYYMM(date) ORDER BY (date)' do |t|
217
+ t.integer :int1, null: false
218
+ t.integer :int2, null: false
219
+ t.date :date, null: false
220
+
221
+ t.index '(int1 * int2, date)', name: 'idx', type: 'minmax', granularity: 3
222
+ end
223
+
224
+ remove_index :some, 'idx'
225
+
226
+ add_index :some, 'int1 * int2', name: 'idx2', type: 'set(10)', granularity: 4
227
+ end
228
+ end
229
+ ```
230
+
231
+ Create table with custom column structure:
232
+
233
+ ``` ruby
234
+ class CreateDataItems < ActiveRecord::Migration
235
+ def change
236
+ create_table "data_items", id: false, options: "MergeTree PARTITION BY toYYYYMM(timestamp) ORDER BY timestamp", force: :cascade do |t|
237
+ t.column "timestamp", "DateTime('UTC') CODEC(DoubleDelta, LZ4)"
238
+ end
215
239
  end
216
240
  end
217
-
218
241
  ```
219
242
 
220
243
 
@@ -246,6 +269,12 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al
246
269
 
247
270
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
248
271
 
272
+ Testing github actions:
273
+
274
+ ```bash
275
+ act
276
+ ```
277
+
249
278
  ## Contributing
250
279
 
251
280
  Bug reports and pull requests are welcome on GitHub at [https://github.com/pnixx/clickhouse-activerecord](https://github.com/pnixx/clickhouse-activerecord). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -6,6 +6,7 @@ module ActiveRecord
6
6
  module OID # :nodoc:
7
7
  class Uuid < Type::Value # :nodoc:
8
8
  ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
9
+ CANONICAL_UUID = %r{\A[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\z}
9
10
 
10
11
  alias :serialize :deserialize
11
12
 
@@ -13,23 +14,32 @@ module ActiveRecord
13
14
  :uuid
14
15
  end
15
16
 
16
- def changed?(old_value, new_value, _)
17
+ def changed?(old_value, new_value, _new_value_before_type_cast)
17
18
  old_value.class != new_value.class ||
18
- new_value && old_value.casecmp(new_value) != 0
19
+ new_value != old_value
19
20
  end
20
21
 
21
22
  def changed_in_place?(raw_old_value, new_value)
22
23
  raw_old_value.class != new_value.class ||
23
- new_value && raw_old_value.casecmp(new_value) != 0
24
+ new_value != raw_old_value
24
25
  end
25
26
 
26
27
  private
27
28
 
28
29
  def cast_value(value)
29
- casted = value.to_s
30
- casted if casted.match?(ACCEPTABLE_UUID)
30
+ value = value.to_s
31
+ format_uuid(value) if value.match?(ACCEPTABLE_UUID)
31
32
  end
32
- end
33
+
34
+ def format_uuid(uuid)
35
+ if uuid.match?(CANONICAL_UUID)
36
+ uuid
37
+ else
38
+ uuid = uuid.delete("{}-").downcase
39
+ "#{uuid[..7]}-#{uuid[8..11]}-#{uuid[12..15]}-#{uuid[16..19]}-#{uuid[20..]}"
40
+ end
41
+ end
42
+ end
33
43
  end
34
44
  end
35
45
  end
@@ -1,4 +1,3 @@
1
- # frozen_string_literal: true
2
1
  begin
3
2
  require "active_record/connection_adapters/deduplicable"
4
3
  rescue LoadError => e
@@ -93,6 +92,14 @@ module ActiveRecord
93
92
 
94
93
  statements = o.columns.map { |c| accept c }
95
94
  statements << accept(o.primary_keys) if o.primary_keys
95
+
96
+ if supports_indexes_in_create?
97
+ indexes = o.indexes.map do |expression, options|
98
+ accept(@conn.add_index_options(o.name, expression, **options))
99
+ end
100
+ statements.concat(indexes)
101
+ end
102
+
96
103
  create_sql << "(#{statements.join(', ')})" if statements.present?
97
104
  # Attach options for only table or materialized view without TO section
98
105
  add_table_options!(create_sql, o) if !o.view || o.view && o.materialized && !o.to
@@ -108,7 +115,7 @@ module ActiveRecord
108
115
 
109
116
  def visit_ChangeColumnDefinition(o)
110
117
  column = o.column
111
- column.sql_type = type_to_sql(column.type, column.options)
118
+ column.sql_type = type_to_sql(column.type, **column.options)
112
119
  options = column_options(column)
113
120
 
114
121
  quoted_column_name = quote_column_name(o.name)
@@ -124,6 +131,23 @@ module ActiveRecord
124
131
  change_column_sql
125
132
  end
126
133
 
134
+ def visit_IndexDefinition(o, create = false)
135
+ sql = create ? ["ALTER TABLE #{quote_table_name(o.table)} ADD"] : []
136
+ sql << "INDEX"
137
+ sql << "IF NOT EXISTS" if o.if_not_exists
138
+ sql << "IF EXISTS" if o.if_exists
139
+ sql << "#{quote_column_name(o.name)} (#{o.expression}) TYPE #{o.type}"
140
+ sql << "GRANULARITY #{o.granularity}" if o.granularity
141
+ sql << "FIRST #{quote_column_name(o.first)}" if o.first
142
+ sql << "AFTER #{quote_column_name(o.after)}" if o.after
143
+
144
+ sql.join(' ')
145
+ end
146
+
147
+ def visit_CreateIndexDefinition(o)
148
+ visit_IndexDefinition(o.index, true)
149
+ end
150
+
127
151
  def current_database
128
152
  ActiveRecord::Base.connection_db_config.database
129
153
  end
@@ -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
@@ -57,6 +57,16 @@ module ActiveRecord
57
57
  result['data'].flatten
58
58
  end
59
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
+
60
70
  def table_options(table)
61
71
  sql = show_create_table(table)
62
72
  { options: sql.gsub(/^(?:.*?)(?:ENGINE = (.*?))?( AS SELECT .*?)?$/, '\\1').presence, as: sql.match(/^CREATE (?:.*?) AS (SELECT .*?)$/).try(:[], 1) }.compact
@@ -67,6 +77,14 @@ module ActiveRecord
67
77
  []
68
78
  end
69
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
+
70
88
  def data_sources
71
89
  tables
72
90
  end
@@ -74,14 +92,14 @@ module ActiveRecord
74
92
  def do_system_execute(sql, name = nil)
75
93
  log_with_debug(sql, "#{adapter_name} #{name}") do
76
94
  res = request(sql, DEFAULT_RESPONSE_FORMAT)
77
- process_response(res, DEFAULT_RESPONSE_FORMAT)
95
+ process_response(res, DEFAULT_RESPONSE_FORMAT, sql)
78
96
  end
79
97
  end
80
98
 
81
99
  def do_execute(sql, name = nil, format: DEFAULT_RESPONSE_FORMAT, settings: {})
82
100
  log(sql, "#{adapter_name} #{name}") do
83
101
  res = request(sql, format, settings)
84
- process_response(res, format)
102
+ process_response(res, format, sql)
85
103
  end
86
104
  end
87
105
 
@@ -132,11 +150,11 @@ module ActiveRecord
132
150
  format ? "#{sql} FORMAT #{format}" : sql
133
151
  end
134
152
 
135
- def process_response(res, format)
153
+ def process_response(res, format, sql = nil)
136
154
  case res.code.to_i
137
155
  when 200
138
156
  if res.body.to_s.include?("DB::Exception")
139
- raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}"
157
+ raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}#{sql ? "\nQuery: #{sql}" : ''}"
140
158
  else
141
159
  format_body_response(res.body, format)
142
160
  end
@@ -233,11 +251,11 @@ module ActiveRecord
233
251
  end
234
252
 
235
253
  def format_from_json_compact(body)
236
- JSON.parse(body)
254
+ parse_json_payload(body)
237
255
  end
238
256
 
239
257
  def format_from_json_compact_each_row_with_names_and_types(body)
240
- rows = body.split("\n").map { |row| JSON.parse(row) }
258
+ rows = body.split("\n").map { |row| parse_json_payload(row) }
241
259
  names, types, *data = rows
242
260
 
243
261
  meta = names.zip(types).map do |name, type|
@@ -252,6 +270,10 @@ module ActiveRecord
252
270
  'data' => data
253
271
  }
254
272
  end
273
+
274
+ def parse_json_payload(payload)
275
+ JSON.parse(payload, decimal_class: BigDecimal)
276
+ end
255
277
  end
256
278
  end
257
279
  end
@@ -149,6 +149,10 @@ module ActiveRecord
149
149
  !native_database_types[type].nil?
150
150
  end
151
151
 
152
+ def supports_indexes_in_create?
153
+ true
154
+ end
155
+
152
156
  class << self
153
157
  def extract_limit(sql_type) # :nodoc:
154
158
  case sql_type
@@ -308,6 +312,11 @@ module ActiveRecord
308
312
  end
309
313
  end
310
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
+
311
320
  # Drops a ClickHouse database.
312
321
  def drop_database(name) #:nodoc:
313
322
  sql = apply_cluster "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
@@ -317,6 +326,12 @@ module ActiveRecord
317
326
  end
318
327
  end
319
328
 
329
+ def drop_functions
330
+ functions.each do |function|
331
+ drop_function(function)
332
+ end
333
+ end
334
+
320
335
  def rename_table(table_name, new_name)
321
336
  do_execute apply_cluster "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
322
337
  end
@@ -336,6 +351,16 @@ module ActiveRecord
336
351
  end
337
352
  end
338
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
+
339
364
  def add_column(table_name, column_name, type, **options)
340
365
  return if options[:if_not_exists] == true && column_exists?(table_name, column_name, type)
341
366
 
@@ -350,8 +375,8 @@ module ActiveRecord
350
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})
351
376
  end
352
377
 
353
- def change_column(table_name, column_name, type, options = {})
354
- 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})
355
380
  raise "Error parse json response: #{result}" if result.presence && !result.is_a?(Hash)
356
381
  end
357
382
 
@@ -365,6 +390,39 @@ module ActiveRecord
365
390
  change_column table_name, column_name, nil, {default: default}.compact
366
391
  end
367
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
+
368
426
  def cluster
369
427
  @config[:cluster_name]
370
428
  end
@@ -420,9 +478,9 @@ module ActiveRecord
420
478
  result
421
479
  end
422
480
 
423
- def change_column_for_alter(table_name, column_name, type, options = {})
481
+ def change_column_for_alter(table_name, column_name, type, **options)
424
482
  td = create_table_definition(table_name)
425
- cd = td.new_column_definition(column_name, type, options)
483
+ cd = td.new_column_definition(column_name, type, **options)
426
484
  schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))
427
485
  end
428
486
 
@@ -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.5'
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.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.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,55 +1,53 @@
1
1
  module CoreExtensions
2
2
  module ActiveRecord
3
3
  module InternalMetadata
4
- module ClassMethods
5
-
6
- def create_table
7
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
8
- return if table_exists? || !enabled?
9
-
10
- key_options = connection.internal_string_options_for_primary_key
11
- table_options = {
12
- id: false,
13
- options: 'ReplacingMergeTree(created_at) PARTITION BY key ORDER BY key',
14
- if_not_exists: true
15
- }
16
- full_config = connection.instance_variable_get(:@config) || {}
17
-
18
- if full_config[:distributed_service_tables]
19
- table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
20
-
21
- distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
22
- else
23
- distributed_suffix = ''
24
- end
25
-
26
- connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
27
- t.string :key, **key_options
28
- t.string :value
29
- t.timestamps
30
- end
31
- end
32
4
 
33
- private
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) || {}
34
16
 
35
- def update_entry(key, new_value)
36
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
17
+ if full_config[:distributed_service_tables]
18
+ table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
37
19
 
38
- create_entry(key, new_value)
20
+ distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
21
+ else
22
+ distributed_suffix = ''
39
23
  end
40
24
 
41
- def select_entry(key)
42
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
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
29
+ end
30
+ end
43
31
 
44
- sm = ::Arel::SelectManager.new(arel_table)
45
- sm.final! if connection.table_options(table_name)[:options] =~ /^ReplacingMergeTree/
46
- sm.project(::Arel.star)
47
- sm.where(arel_table[primary_key].eq(::Arel::Nodes::BindParam.new(key)))
48
- sm.order(arel_table[primary_key].asc)
49
- sm.limit = 1
32
+ private
50
33
 
51
- connection.select_one(sm, "#{self.class} Load")
52
- end
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")
53
51
  end
54
52
  end
55
53
  end
@@ -1,47 +1,45 @@
1
1
  module CoreExtensions
2
2
  module ActiveRecord
3
3
  module SchemaMigration
4
- module ClassMethods
5
4
 
6
- def create_table
7
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
5
+ def create_table
6
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
8
7
 
9
- return if table_exists?
8
+ return if table_exists?
10
9
 
11
- version_options = connection.internal_string_options_for_primary_key
12
- table_options = {
13
- id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true
14
- }
15
- full_config = connection.instance_variable_get(:@config) || {}
10
+ version_options = connection.internal_string_options_for_primary_key
11
+ table_options = {
12
+ id: false, options: 'ReplacingMergeTree(ver) ORDER BY (version)', if_not_exists: true
13
+ }
14
+ full_config = connection.instance_variable_get(:@config) || {}
16
15
 
17
- if full_config[:distributed_service_tables]
18
- table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)')
16
+ if full_config[:distributed_service_tables]
17
+ table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)')
19
18
 
20
- distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
21
- else
22
- distributed_suffix = ''
23
- end
19
+ distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
20
+ else
21
+ distributed_suffix = ''
22
+ end
24
23
 
25
- connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
26
- t.string :version, **version_options
27
- t.column :active, 'Int8', null: false, default: '1'
28
- t.datetime :ver, null: false, default: -> { 'now()' }
29
- end
24
+ connection.create_table(table_name + distributed_suffix.to_s, **table_options) do |t|
25
+ t.string :version, **version_options
26
+ t.column :active, 'Int8', null: false, default: '1'
27
+ t.datetime :ver, null: false, default: -> { 'now()' }
30
28
  end
29
+ end
31
30
 
32
- def delete_version(version)
33
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
31
+ def delete_version(version)
32
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
34
33
 
35
- im = ::Arel::InsertManager.new(arel_table)
36
- im.insert(arel_table[primary_key] => version.to_s, arel_table['active'] => 0)
37
- connection.insert(im, "#{self.class} Create Rollback Version", primary_key, version)
38
- end
34
+ im = ::Arel::InsertManager.new(arel_table)
35
+ im.insert(arel_table[primary_key] => version.to_s, arel_table['active'] => 0)
36
+ connection.insert(im, "#{self.class} Create Rollback Version", primary_key, version)
37
+ end
39
38
 
40
- def all_versions
41
- return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
39
+ def all_versions
40
+ return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
42
41
 
43
- final.where(active: 1).order(:version).pluck(:version)
44
- end
42
+ final.where(active: 1).order(:version).pluck(:version)
45
43
  end
46
44
  end
47
45
  end
@@ -15,15 +15,18 @@ namespace :clickhouse do
15
15
  # TODO: deprecated
16
16
  desc 'Load database schema'
17
17
  task load: %i[prepare_internal_metadata_table] do
18
+ puts 'Warning: `rake clickhouse:schema:load` is deprecated! Use `rake db:schema:load:clickhouse` instead'
18
19
  simple = ENV['simple'] || ARGV.any? { |a| a.include?('--simple') } ? '_simple' : nil
19
20
  ActiveRecord::Base.establish_connection(:clickhouse)
20
- ActiveRecord::SchemaMigration.drop_table
21
+ connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection
22
+ connection.schema_migration.drop_table
21
23
  load(Rails.root.join("db/clickhouse_schema#{simple}.rb"))
22
24
  end
23
25
 
24
26
  # TODO: deprecated
25
27
  desc 'Dump database schema'
26
28
  task dump: :environment do |_, args|
29
+ puts 'Warning: `rake clickhouse:schema:dump` is deprecated! Use `rake db:schema:dump:clickhouse` instead'
27
30
  simple = ENV['simple'] || args[:simple] || ARGV.any? { |a| a.include?('--simple') } ? '_simple' : nil
28
31
  filename = Rails.root.join("db/clickhouse_schema#{simple}.rb")
29
32
  File.open(filename, 'w:utf-8') do |file|
@@ -36,43 +39,38 @@ namespace :clickhouse do
36
39
  namespace :structure do
37
40
  desc 'Load database structure'
38
41
  task load: ['db:check_protected_environments'] do
39
- config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
40
42
  ClickhouseActiverecord::Tasks.new(config).structure_load(Rails.root.join('db/clickhouse_structure.sql'))
41
43
  end
42
44
 
43
45
  desc 'Dump database structure'
44
46
  task dump: ['db:check_protected_environments'] do
45
- config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
46
47
  ClickhouseActiverecord::Tasks.new(config).structure_dump(Rails.root.join('db/clickhouse_structure.sql'))
47
48
  end
48
49
  end
49
50
 
50
51
  desc 'Creates the database from DATABASE_URL or config/database.yml'
51
52
  task create: [] do
52
- config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
53
- ActiveRecord::Tasks::DatabaseTasks.create(config)
53
+ puts 'Warning: `rake clickhouse:create` is deprecated! Use `rake db:create:clickhouse` instead'
54
54
  end
55
55
 
56
56
  desc 'Drops the database from DATABASE_URL or config/database.yml'
57
57
  task drop: ['db:check_protected_environments'] do
58
- config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
59
- ActiveRecord::Tasks::DatabaseTasks.drop(config)
58
+ puts 'Warning: `rake clickhouse:drop` is deprecated! Use `rake db:drop:clickhouse` instead'
60
59
  end
61
60
 
62
61
  desc 'Empty the database from DATABASE_URL or config/database.yml'
63
62
  task purge: ['db:check_protected_environments'] do
64
- config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
65
- ActiveRecord::Tasks::DatabaseTasks.purge(config)
63
+ puts 'Warning: `rake clickhouse:purge` is deprecated! Use `rake db:reset:clickhouse` instead'
66
64
  end
67
65
 
68
66
  # desc 'Resets your database using your migrations for the current environment'
69
67
  task :reset do
70
- Rake::Task['clickhouse:purge'].execute
71
- Rake::Task['clickhouse:migrate'].execute
68
+ puts 'Warning: `rake clickhouse:reset` is deprecated! Use `rake db:reset:clickhouse` instead'
72
69
  end
73
70
 
74
71
  desc 'Migrate the clickhouse database'
75
72
  task migrate: %i[prepare_schema_migration_table prepare_internal_metadata_table] do
73
+ puts 'Warning: `rake clickhouse:migrate` is deprecated! Use `rake db:migrate:clickhouse` instead'
76
74
  Rake::Task['db:migrate:clickhouse'].execute
77
75
  if File.exist? "#{Rails.root}/db/clickhouse_schema_simple.rb"
78
76
  Rake::Task['clickhouse:schema:dump'].execute(simple: true)
@@ -81,9 +79,14 @@ namespace :clickhouse do
81
79
 
82
80
  desc 'Rollback the clickhouse database'
83
81
  task rollback: %i[prepare_schema_migration_table prepare_internal_metadata_table] do
82
+ puts 'Warning: `rake clickhouse:rollback` is deprecated! Use `rake db:rollback:clickhouse` instead'
84
83
  Rake::Task['db:rollback:clickhouse'].execute
85
84
  if File.exist? "#{Rails.root}/db/clickhouse_schema_simple.rb"
86
85
  Rake::Task['clickhouse:schema:dump'].execute(simple: true)
87
86
  end
88
87
  end
88
+
89
+ def config
90
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'clickhouse')
91
+ end
89
92
  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: 1.0.5
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-14 00:00:00.000000000 Z
11
+ date: 2024-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -106,7 +106,6 @@ files:
106
106
  - bin/console
107
107
  - bin/setup
108
108
  - clickhouse-activerecord.gemspec
109
- - core_extensions/active_record/migration/command_recorder.rb
110
109
  - lib/active_record/connection_adapters/clickhouse/oid/array.rb
111
110
  - lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
112
111
  - lib/active_record/connection_adapters/clickhouse/oid/date.rb
@@ -127,6 +126,7 @@ files:
127
126
  - lib/clickhouse-activerecord/tasks.rb
128
127
  - lib/clickhouse-activerecord/version.rb
129
128
  - lib/core_extensions/active_record/internal_metadata.rb
129
+ - lib/core_extensions/active_record/migration/command_recorder.rb
130
130
  - lib/core_extensions/active_record/relation.rb
131
131
  - lib/core_extensions/active_record/schema_migration.rb
132
132
  - lib/core_extensions/arel/nodes/select_core.rb
@@ -154,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
154
  - !ruby/object:Gem::Version
155
155
  version: '0'
156
156
  requirements: []
157
- rubygems_version: 3.1.6
157
+ rubygems_version: 3.3.7
158
158
  signing_key:
159
159
  specification_version: 4
160
160
  summary: ClickHouse ActiveRecord