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 +4 -4
- data/.docker/docker-compose.cluster.yml +11 -0
- data/.docker/docker-compose.yml +3 -0
- data/CHANGELOG.md +8 -0
- data/README.md +31 -2
- data/lib/active_record/connection_adapters/clickhouse/oid/uuid.rb +16 -6
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +26 -2
- data/lib/active_record/connection_adapters/clickhouse/schema_definitions.rb +17 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +28 -6
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +62 -4
- data/lib/clickhouse-activerecord/schema_dumper.rb +30 -2
- data/lib/clickhouse-activerecord/tasks.rb +26 -6
- data/lib/clickhouse-activerecord/version.rb +1 -1
- data/lib/clickhouse-activerecord.rb +4 -6
- data/lib/core_extensions/active_record/internal_metadata.rb +40 -42
- data/lib/core_extensions/active_record/schema_migration.rb +28 -30
- data/lib/tasks/clickhouse.rake +14 -11
- metadata +4 -4
- /data/{core_extensions → lib/core_extensions}/active_record/migration/command_recorder.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17575531d9c1ae85c10c20230bb78014f9f0e07affe672b683b66651e37af812
|
4
|
+
data.tar.gz: 14ac2cfb7e6cfa344cf2284a76082e5476e08cfe77a499695157e09c58cd7765
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/.docker/docker-compose.yml
CHANGED
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
|
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
|
24
|
+
new_value != raw_old_value
|
24
25
|
end
|
25
26
|
|
26
27
|
private
|
27
28
|
|
28
29
|
def cast_value(value)
|
29
|
-
|
30
|
-
|
30
|
+
value = value.to_s
|
31
|
+
format_uuid(value) if value.match?(ACCEPTABLE_UUID)
|
31
32
|
end
|
32
|
-
|
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
|
-
|
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|
|
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
|
-
|
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
|
-
|
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
|
12
|
+
@configuration = configuration
|
9
13
|
end
|
10
14
|
|
11
15
|
def create
|
12
16
|
establish_master_connection
|
13
|
-
connection.create_database @configuration
|
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
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
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
|
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
|
-
|
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
|
-
|
36
|
-
|
17
|
+
if full_config[:distributed_service_tables]
|
18
|
+
table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(created_at)')
|
37
19
|
|
38
|
-
|
20
|
+
distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
|
21
|
+
else
|
22
|
+
distributed_suffix = ''
|
39
23
|
end
|
40
24
|
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
7
|
-
|
5
|
+
def create_table
|
6
|
+
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
|
8
7
|
|
9
|
-
|
8
|
+
return if table_exists?
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
16
|
+
if full_config[:distributed_service_tables]
|
17
|
+
table_options.merge!(with_distributed: table_name, sharding_key: 'cityHash64(version)')
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
distributed_suffix = "_#{full_config[:distributed_service_tables_suffix] || 'distributed'}"
|
20
|
+
else
|
21
|
+
distributed_suffix = ''
|
22
|
+
end
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
31
|
+
def delete_version(version)
|
32
|
+
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
39
|
+
def all_versions
|
40
|
+
return super unless connection.is_a?(::ActiveRecord::ConnectionAdapters::ClickhouseAdapter)
|
42
41
|
|
43
|
-
|
44
|
-
end
|
42
|
+
final.where(active: 1).order(:version).pluck(:version)
|
45
43
|
end
|
46
44
|
end
|
47
45
|
end
|
data/lib/tasks/clickhouse.rake
CHANGED
@@ -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::
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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.
|
157
|
+
rubygems_version: 3.3.7
|
158
158
|
signing_key:
|
159
159
|
specification_version: 4
|
160
160
|
summary: ClickHouse ActiveRecord
|
File without changes
|