clickhouse-activerecord 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2217c1539b22307398fbc9c01bb8b521300ca23a7d03c2d32a56a9abdd3c5546
4
- data.tar.gz: 8aa60f1a78db38df4120b0b405ddaf9466da70e68bf382f58945b29d5e130b7b
3
+ metadata.gz: 17575531d9c1ae85c10c20230bb78014f9f0e07affe672b683b66651e37af812
4
+ data.tar.gz: 14ac2cfb7e6cfa344cf2284a76082e5476e08cfe77a499695157e09c58cd7765
5
5
  SHA512:
6
- metadata.gz: d1e48e302475eff9ca738b35fc56b58a7a73f7d6b6cf5ae6632a7ee95fbc9432734ba18c53f7756d833ebd3748368fd73f7c29e04a0972f3141d00b5acd262cd
7
- data.tar.gz: acedbb7298cd044769a5a1deaaae44672bdbf1affca5d2218e8b7209731df505434ed4f69bb23881e0d4f231e9e6699ac621ab0b4ac19f86475dd6c6515b3e18
6
+ metadata.gz: '09973aaa310292cfdef323b4c66b2088db297b243b90e6149e7f6998165a9208adf74cbeb2665f0230ee6ac8dacc1632e1562eaff085bf07d6689ccfbcc0d75c'
7
+ data.tar.gz: 38b8ba94d466720ff860a62a2542d1cb2ec106c88ffd000979203e8c9403e0aa42d0f4ba3e83e822270888a11b07f3b96296785c0fdc7b1b39fa12e019b07e6e
@@ -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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### Version 1.0.5 (Apr 26, 2024)
2
+
3
+ * Support table indexes
4
+ * Fix non-canonical UUID by [@PauloMiranda98](https://github.com/PauloMiranda98) in (#117)
5
+ * Fix precision loss due to JSON float parsing by [@jenskdsgn](https://github.com/jenskdsgn) in (#129)
6
+ * Support functions by [@felix-dumit](https://github.com/felix-dumit) in (#120)
7
+ * Hotfix/rails71 change column by [@trumenov](https://github.com/trumenov) in (#132)
8
+
1
9
  ### Version 1.0.5 (Mar 14, 2024)
2
10
 
3
11
  * GitHub workflows
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
@@ -4,13 +4,17 @@ module ClickhouseActiverecord
4
4
  class Tasks
5
5
  delegate :connection, :establish_connection, to: ActiveRecord::Base
6
6
 
7
+ def self.using_database_configurations?
8
+ true
9
+ end
10
+
7
11
  def initialize(configuration)
8
- @configuration = configuration.with_indifferent_access
12
+ @configuration = configuration
9
13
  end
10
14
 
11
15
  def create
12
16
  establish_master_connection
13
- connection.create_database @configuration['database']
17
+ connection.create_database @configuration.database
14
18
  rescue ActiveRecord::StatementInvalid => e
15
19
  if e.cause.to_s.include?('already exists')
16
20
  raise ActiveRecord::DatabaseAlreadyExists
@@ -21,7 +25,7 @@ module ClickhouseActiverecord
21
25
 
22
26
  def drop
23
27
  establish_master_connection
24
- connection.drop_database @configuration['database']
28
+ connection.drop_database @configuration.database
25
29
  end
26
30
 
27
31
  def purge
@@ -31,12 +35,28 @@ module ClickhouseActiverecord
31
35
  end
32
36
 
33
37
  def structure_dump(*args)
34
- tables = connection.execute("SHOW TABLES FROM #{@configuration['database']}")['data'].flatten
38
+ establish_master_connection
39
+
40
+ # get all tables
41
+ tables = connection.execute("SHOW TABLES FROM #{@configuration.database} WHERE name NOT LIKE '.inner_id.%'")['data'].flatten.map do |table|
42
+ next if %w[schema_migrations ar_internal_metadata].include?(table)
43
+ connection.show_create_table(table).gsub("#{@configuration.database}.", '')
44
+ end.compact
35
45
 
46
+ # sort view to last
47
+ tables.sort_by! {|table| table.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : 0}
48
+
49
+ # get all functions
50
+ functions = connection.execute("SELECT create_query FROM system.functions WHERE origin = 'SQLUserDefined'")['data'].flatten
51
+
52
+ # put to file
36
53
  File.open(args.first, 'w:utf-8') do |file|
54
+ functions.each do |function|
55
+ file.puts function + ";\n\n"
56
+ end
57
+
37
58
  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"
59
+ file.puts table + ";\n\n"
40
60
  end
41
61
  end
42
62
  end
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '1.0.5'
2
+ VERSION = '1.0.7'
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.7
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-27 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