clickhouse-activerecord 1.1.3 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/testing.yml +3 -3
- data/README.md +2 -1
- data/lib/active_record/connection_adapters/clickhouse/column.rb +21 -0
- data/lib/active_record/connection_adapters/clickhouse/oid/map.rb +27 -9
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +7 -1
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +13 -3
- data/lib/active_record/connection_adapters/clickhouse/{schema_definitions.rb → table_definition.rb} +6 -1
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +18 -15
- data/lib/arel/nodes/grouping_sets.rb +32 -0
- data/lib/arel/nodes/limit_by.rb +17 -0
- data/lib/arel/visitors/clickhouse.rb +30 -0
- data/lib/clickhouse-activerecord/schema_dumper.rb +38 -13
- data/lib/clickhouse-activerecord/tasks.rb +4 -2
- data/lib/clickhouse-activerecord/version.rb +1 -1
- data/lib/core_extensions/active_record/relation.rb +46 -0
- data/lib/core_extensions/arel/nodes/select_statement.rb +5 -2
- data/lib/core_extensions/arel/select_manager.rb +5 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b40a04fe93423fd3469b65a66429ecb41e4c5c25440777c69617e69081e578b4
|
4
|
+
data.tar.gz: 43586cb853dec4d3453a6b6938158e5f1197504678c2e1aa824b8521a6a1c628
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38073e81994dd027caf7fab5463a715eac7f15ec8373c1415d0064611118b0f2b0c391cae0db9b7670ebb8a3141c023879b3d1425c7059f4c00450579bcff64c
|
7
|
+
data.tar.gz: 0ce78412004ae3590bc97877141e66f74f9de1ad0aab2cdc1af5a3369157c01e0e29fe10b4921c96487cad66fa16e50163f9c34941381357f1960cc3c7f20fbc
|
@@ -27,7 +27,7 @@ jobs:
|
|
27
27
|
- ruby: 3.2
|
28
28
|
rails: 7.1.3
|
29
29
|
- ruby: 3.2
|
30
|
-
rails: 7.2.
|
30
|
+
rails: 7.2.1
|
31
31
|
clickhouse: [ '22.1', '24.6' ]
|
32
32
|
|
33
33
|
steps:
|
@@ -49,7 +49,7 @@ jobs:
|
|
49
49
|
ruby-version: ${{ matrix.version.ruby }}
|
50
50
|
bundler-cache: true
|
51
51
|
|
52
|
-
- run: bundle exec rspec spec/single
|
52
|
+
- run: bundle exec rspec spec/single --format progress
|
53
53
|
|
54
54
|
tests_cluster:
|
55
55
|
name: Testing cluster server
|
@@ -94,4 +94,4 @@ jobs:
|
|
94
94
|
ruby-version: ${{ matrix.version.ruby }}
|
95
95
|
bundler-cache: true
|
96
96
|
|
97
|
-
- run: bundle exec rspec spec/cluster
|
97
|
+
- run: bundle exec rspec spec/cluster --format progress
|
data/README.md
CHANGED
@@ -237,12 +237,13 @@ class CreateDataItems < ActiveRecord::Migration[7.1]
|
|
237
237
|
end
|
238
238
|
```
|
239
239
|
|
240
|
-
Create table with custom column structure:
|
240
|
+
Create table with custom column structure and codec compression:
|
241
241
|
|
242
242
|
```ruby
|
243
243
|
class CreateDataItems < ActiveRecord::Migration[7.1]
|
244
244
|
def change
|
245
245
|
create_table "data_items", id: false, options: "MergeTree PARTITION BY toYYYYMM(timestamp) ORDER BY timestamp", force: :cascade do |t|
|
246
|
+
t.integer :user_id, limit: 8, codec: 'DoubleDelta, LZ4'
|
246
247
|
t.column "timestamp", "DateTime('UTC') CODEC(DoubleDelta, LZ4)"
|
247
248
|
end
|
248
249
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Clickhouse
|
4
|
+
class Column < ActiveRecord::ConnectionAdapters::Column
|
5
|
+
|
6
|
+
attr_reader :codec
|
7
|
+
|
8
|
+
def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, codec: nil, **args)
|
9
|
+
super
|
10
|
+
@codec = codec
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def deduplicated
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -7,15 +7,16 @@ module ActiveRecord
|
|
7
7
|
class Map < Type::Value # :nodoc:
|
8
8
|
|
9
9
|
def initialize(sql_type)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
case sql_type
|
11
|
+
when /U?Int(\d+)/
|
12
|
+
@subtype = :integer
|
13
|
+
@limit = bits_to_limit(Regexp.last_match(1)&.to_i)
|
14
|
+
when /DateTime/
|
15
|
+
@subtype = :datetime
|
16
|
+
when /Date/
|
17
|
+
@subtype = :date
|
18
|
+
else
|
19
|
+
@subtype = :string
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
@@ -26,6 +27,8 @@ module ActiveRecord
|
|
26
27
|
def deserialize(value)
|
27
28
|
if value.is_a?(::Hash)
|
28
29
|
value.map { |k, item| [k.to_s, deserialize(item)] }.to_h
|
30
|
+
elsif value.is_a?(::Array)
|
31
|
+
value.map { |item| deserialize(item) }
|
29
32
|
else
|
30
33
|
return value if value.nil?
|
31
34
|
case @subtype
|
@@ -44,6 +47,8 @@ module ActiveRecord
|
|
44
47
|
def serialize(value)
|
45
48
|
if value.is_a?(::Hash)
|
46
49
|
value.map { |k, item| [k.to_s, serialize(item)] }.to_h
|
50
|
+
elsif value.is_a?(::Array)
|
51
|
+
value.map { |item| serialize(item) }
|
47
52
|
else
|
48
53
|
return value if value.nil?
|
49
54
|
case @subtype
|
@@ -61,6 +66,19 @@ module ActiveRecord
|
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
69
|
+
private
|
70
|
+
|
71
|
+
def bits_to_limit(bits)
|
72
|
+
case bits
|
73
|
+
when 8 then 1
|
74
|
+
when 16 then 2
|
75
|
+
when 32 then 4
|
76
|
+
when 64 then 8
|
77
|
+
when 128 then 16
|
78
|
+
when 256 then 32
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
64
82
|
end
|
65
83
|
end
|
66
84
|
end
|
@@ -33,9 +33,15 @@ module ActiveRecord
|
|
33
33
|
if options[:array]
|
34
34
|
sql.gsub!(/\s+(.*)/, ' Array(\1)')
|
35
35
|
end
|
36
|
-
if options[:map]
|
36
|
+
if options[:map] == :array
|
37
|
+
sql.gsub!(/\s+(.*)/, ' Map(String, Array(\1))')
|
38
|
+
end
|
39
|
+
if options[:map] == true
|
37
40
|
sql.gsub!(/\s+(.*)/, ' Map(String, \1)')
|
38
41
|
end
|
42
|
+
if options[:codec]
|
43
|
+
sql.gsub!(/\s+(.*)/, " \\1 CODEC(#{options[:codec]})")
|
44
|
+
end
|
39
45
|
sql.gsub!(/(\sString)\(\d+\)/, '\1')
|
40
46
|
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
|
41
47
|
sql
|
@@ -8,6 +8,8 @@ module ActiveRecord
|
|
8
8
|
module SchemaStatements
|
9
9
|
DEFAULT_RESPONSE_FORMAT = 'JSONCompactEachRowWithNamesAndTypes'.freeze
|
10
10
|
|
11
|
+
DB_EXCEPTION_REGEXP = /\ACode:\s+\d+\.\s+DB::Exception:/.freeze
|
12
|
+
|
11
13
|
def execute(sql, name = nil, settings: {})
|
12
14
|
do_execute(sql, name, settings: settings)
|
13
15
|
end
|
@@ -70,8 +72,14 @@ module ActiveRecord
|
|
70
72
|
result['data'].flatten
|
71
73
|
end
|
72
74
|
|
75
|
+
def materialized_views(name = nil)
|
76
|
+
result = do_system_execute("SHOW TABLES WHERE engine = 'MaterializedView'", name)
|
77
|
+
return [] if result.nil?
|
78
|
+
result['data'].flatten
|
79
|
+
end
|
80
|
+
|
73
81
|
def functions
|
74
|
-
result = do_system_execute("SELECT name FROM system.functions WHERE origin = 'SQLUserDefined'")
|
82
|
+
result = do_system_execute("SELECT name FROM system.functions WHERE origin = 'SQLUserDefined' ORDER BY name")
|
75
83
|
return [] if result.nil?
|
76
84
|
result['data'].flatten
|
77
85
|
end
|
@@ -183,7 +191,9 @@ module ActiveRecord
|
|
183
191
|
def process_response(res, format, sql = nil)
|
184
192
|
case res.code.to_i
|
185
193
|
when 200
|
186
|
-
|
194
|
+
body = res.body
|
195
|
+
|
196
|
+
if body.include?("DB::Exception") && body.match?(DB_EXCEPTION_REGEXP)
|
187
197
|
raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}#{sql ? "\nQuery: #{sql}" : ''}"
|
188
198
|
else
|
189
199
|
format_body_response(res.body, format)
|
@@ -221,7 +231,7 @@ module ActiveRecord
|
|
221
231
|
default_value = extract_value_from_default(field[3], field[2])
|
222
232
|
default_function = extract_default_function(field[3])
|
223
233
|
default_value = lookup_cast_type(sql_type).cast(default_value)
|
224
|
-
|
234
|
+
Clickhouse::Column.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), default_function, codec: field[5].presence)
|
225
235
|
end
|
226
236
|
|
227
237
|
protected
|
data/lib/active_record/connection_adapters/clickhouse/{schema_definitions.rb → table_definition.rb}
RENAMED
@@ -94,10 +94,15 @@ module ActiveRecord
|
|
94
94
|
args.each { |name| column(name, kind, **options.except(:limit)) }
|
95
95
|
end
|
96
96
|
|
97
|
+
def column(name, type, index: nil, **options)
|
98
|
+
options[:null] = false if type.match?(/Nullable\([^)]+\)/)
|
99
|
+
super(name, type, index: index, **options)
|
100
|
+
end
|
101
|
+
|
97
102
|
private
|
98
103
|
|
99
104
|
def valid_column_definition_options
|
100
|
-
super + [:array, :low_cardinality, :fixed_string, :value, :type, :map]
|
105
|
+
super + [:array, :low_cardinality, :fixed_string, :value, :type, :map, :codec, :unsigned]
|
101
106
|
end
|
102
107
|
end
|
103
108
|
|
@@ -2,18 +2,21 @@
|
|
2
2
|
|
3
3
|
require 'arel/visitors/clickhouse'
|
4
4
|
require 'arel/nodes/final'
|
5
|
+
require 'arel/nodes/grouping_sets'
|
5
6
|
require 'arel/nodes/settings'
|
6
7
|
require 'arel/nodes/using'
|
8
|
+
require 'arel/nodes/limit_by'
|
7
9
|
require 'active_record/connection_adapters/clickhouse/oid/array'
|
8
10
|
require 'active_record/connection_adapters/clickhouse/oid/date'
|
9
11
|
require 'active_record/connection_adapters/clickhouse/oid/date_time'
|
10
12
|
require 'active_record/connection_adapters/clickhouse/oid/big_integer'
|
11
13
|
require 'active_record/connection_adapters/clickhouse/oid/map'
|
12
14
|
require 'active_record/connection_adapters/clickhouse/oid/uuid'
|
15
|
+
require 'active_record/connection_adapters/clickhouse/column'
|
13
16
|
require 'active_record/connection_adapters/clickhouse/quoting'
|
14
|
-
require 'active_record/connection_adapters/clickhouse/schema_definitions'
|
15
17
|
require 'active_record/connection_adapters/clickhouse/schema_creation'
|
16
18
|
require 'active_record/connection_adapters/clickhouse/schema_statements'
|
19
|
+
require 'active_record/connection_adapters/clickhouse/table_definition'
|
17
20
|
require 'net/http'
|
18
21
|
require 'openssl'
|
19
22
|
|
@@ -47,7 +50,12 @@ module ActiveRecord
|
|
47
50
|
|
48
51
|
module ModelSchema
|
49
52
|
module ClassMethods
|
50
|
-
delegate :final, :final!,
|
53
|
+
delegate :final, :final!,
|
54
|
+
:group_by_grouping_sets, :group_by_grouping_sets!,
|
55
|
+
:settings, :settings!,
|
56
|
+
:window, :window!,
|
57
|
+
:limit_by, :limit_by!,
|
58
|
+
to: :all
|
51
59
|
|
52
60
|
def is_view
|
53
61
|
@is_view || false
|
@@ -70,13 +78,6 @@ module ActiveRecord
|
|
70
78
|
register "clickhouse", "ActiveRecord::ConnectionAdapters::ClickhouseAdapter", "active_record/connection_adapters/clickhouse_adapter"
|
71
79
|
end
|
72
80
|
|
73
|
-
class ClickhouseColumn < Column
|
74
|
-
private
|
75
|
-
def deduplicated
|
76
|
-
self
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
81
|
class ClickhouseAdapter < AbstractAdapter
|
81
82
|
include Clickhouse::Quoting
|
82
83
|
|
@@ -190,6 +191,8 @@ module ActiveRecord
|
|
190
191
|
nil
|
191
192
|
when /(Nullable)?\(?U?Int64\)?/
|
192
193
|
8
|
194
|
+
when /(Nullable)?\(?U?Int128\)?/
|
195
|
+
16
|
193
196
|
else
|
194
197
|
super
|
195
198
|
end
|
@@ -311,7 +314,7 @@ module ActiveRecord
|
|
311
314
|
end
|
312
315
|
end
|
313
316
|
|
314
|
-
def create_view(table_name, **options)
|
317
|
+
def create_view(table_name, request_settings: {}, **options)
|
315
318
|
options.merge!(view: true)
|
316
319
|
options = apply_replica(table_name, options)
|
317
320
|
td = create_table_definition(apply_cluster(table_name), **options)
|
@@ -321,10 +324,10 @@ module ActiveRecord
|
|
321
324
|
drop_table(table_name, options.merge(if_exists: true))
|
322
325
|
end
|
323
326
|
|
324
|
-
do_execute(schema_creation.accept(td), format: nil)
|
327
|
+
do_execute(schema_creation.accept(td), format: nil, settings: request_settings)
|
325
328
|
end
|
326
329
|
|
327
|
-
def create_table(table_name, **options, &block)
|
330
|
+
def create_table(table_name, request_settings: {}, **options, &block)
|
328
331
|
options = apply_replica(table_name, options)
|
329
332
|
td = create_table_definition(apply_cluster(table_name), **options)
|
330
333
|
block.call td if block_given?
|
@@ -338,7 +341,7 @@ module ActiveRecord
|
|
338
341
|
drop_table(table_name, options.merge(if_exists: true))
|
339
342
|
end
|
340
343
|
|
341
|
-
do_execute(schema_creation.accept(td), format: nil)
|
344
|
+
do_execute(schema_creation.accept(td), format: nil, settings: request_settings)
|
342
345
|
|
343
346
|
if options[:with_distributed]
|
344
347
|
distributed_table_name = options.delete(:with_distributed)
|
@@ -351,8 +354,8 @@ module ActiveRecord
|
|
351
354
|
end
|
352
355
|
end
|
353
356
|
|
354
|
-
def create_function(name, body)
|
355
|
-
fd = "CREATE FUNCTION #{apply_cluster(quote_table_name(name))} AS #{body}"
|
357
|
+
def create_function(name, body, **options)
|
358
|
+
fd = "CREATE#{' OR REPLACE' if options[:force]} FUNCTION #{apply_cluster(quote_table_name(name))} AS #{body}"
|
356
359
|
do_execute(fd, format: nil)
|
357
360
|
end
|
358
361
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Arel # :nodoc: all
|
4
|
+
module Nodes
|
5
|
+
class GroupingSets < Arel::Nodes::Unary
|
6
|
+
|
7
|
+
def initialize(expr)
|
8
|
+
super
|
9
|
+
@expr = wrap_grouping_sets(expr)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def wrap_grouping_sets(sets)
|
15
|
+
sets.map do |element|
|
16
|
+
# See Arel::SelectManager#group
|
17
|
+
case element
|
18
|
+
when Array
|
19
|
+
wrap_grouping_sets(element)
|
20
|
+
when String
|
21
|
+
::Arel::Nodes::SqlLiteral.new(element)
|
22
|
+
when Symbol
|
23
|
+
::Arel::Nodes::SqlLiteral.new(element.to_s)
|
24
|
+
else
|
25
|
+
element
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Arel # :nodoc: all
|
2
|
+
module Nodes
|
3
|
+
class LimitBy < Arel::Nodes::Unary
|
4
|
+
attr_reader :column
|
5
|
+
|
6
|
+
def initialize(limit, column)
|
7
|
+
raise ArgumentError, 'Limit should be an integer' unless limit.is_a?(Integer)
|
8
|
+
raise ArgumentError, 'Limit should be a positive integer' unless limit >= 0
|
9
|
+
raise ArgumentError, 'Column should be a Symbol or String' unless column.is_a?(String) || column.is_a?(Symbol)
|
10
|
+
|
11
|
+
@column = column
|
12
|
+
|
13
|
+
super(limit)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -25,6 +25,7 @@ module Arel
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def visit_Arel_Nodes_SelectOptions(o, collector)
|
28
|
+
maybe_visit o.limit_by, collector
|
28
29
|
maybe_visit o.settings, super
|
29
30
|
end
|
30
31
|
|
@@ -45,6 +46,11 @@ module Arel
|
|
45
46
|
collector
|
46
47
|
end
|
47
48
|
|
49
|
+
def visit_Arel_Nodes_GroupingSets(o, collector)
|
50
|
+
collector << 'GROUPING SETS '
|
51
|
+
grouping_array_or_grouping_element(o.expr, collector)
|
52
|
+
end
|
53
|
+
|
48
54
|
def visit_Arel_Nodes_Settings(o, collector)
|
49
55
|
return collector if o.expr.empty?
|
50
56
|
|
@@ -64,6 +70,11 @@ module Arel
|
|
64
70
|
collector
|
65
71
|
end
|
66
72
|
|
73
|
+
def visit_Arel_Nodes_LimitBy(o, collector)
|
74
|
+
collector << "LIMIT #{o.expr} BY #{o.column}"
|
75
|
+
collector
|
76
|
+
end
|
77
|
+
|
67
78
|
def visit_Arel_Nodes_Matches(o, collector)
|
68
79
|
op = o.case_sensitive ? " LIKE " : " ILIKE "
|
69
80
|
infix_value o, collector, op
|
@@ -95,6 +106,25 @@ module Arel
|
|
95
106
|
@connection.sanitize_as_setting_name(value)
|
96
107
|
end
|
97
108
|
|
109
|
+
private
|
110
|
+
|
111
|
+
# Utilized by GroupingSet, Cube & RollUp visitors to
|
112
|
+
# handle grouping aggregation semantics
|
113
|
+
def grouping_array_or_grouping_element(o, collector)
|
114
|
+
if o.is_a? Array
|
115
|
+
collector << '( '
|
116
|
+
o.each_with_index do |el, i|
|
117
|
+
collector << ', ' if i > 0
|
118
|
+
grouping_array_or_grouping_element el, collector
|
119
|
+
end
|
120
|
+
collector << ' )'
|
121
|
+
elsif o.respond_to? :expr
|
122
|
+
visit o.expr, collector
|
123
|
+
else
|
124
|
+
visit o, collector
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
98
128
|
end
|
99
129
|
end
|
100
130
|
end
|
@@ -15,13 +15,16 @@ module ClickhouseActiverecord
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def tables(stream)
|
18
|
-
functions = @connection.functions
|
18
|
+
functions = @connection.functions.sort
|
19
19
|
functions.each do |function|
|
20
20
|
function(function, stream)
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
view_tables = @connection.views.sort
|
24
|
+
materialized_view_tables = @connection.materialized_views.sort
|
25
|
+
sorted_tables = @connection.tables.sort - view_tables - materialized_view_tables
|
26
|
+
|
27
|
+
(sorted_tables + view_tables + materialized_view_tables).each do |table_name|
|
25
28
|
table(table_name, stream) unless ignored?(table_name)
|
26
29
|
end
|
27
30
|
end
|
@@ -35,7 +38,7 @@ module ClickhouseActiverecord
|
|
35
38
|
# super(table.gsub(/^\.inner\./, ''), stream)
|
36
39
|
|
37
40
|
# detect view table
|
38
|
-
|
41
|
+
view_match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW\s+\S+\s+(?:TO (\S+))?/)
|
39
42
|
end
|
40
43
|
|
41
44
|
# Copy from original dumper
|
@@ -50,8 +53,9 @@ module ClickhouseActiverecord
|
|
50
53
|
|
51
54
|
unless simple
|
52
55
|
# Add materialize flag
|
53
|
-
tbl.print ', view: true' if
|
54
|
-
tbl.print ', materialized: true' if
|
56
|
+
tbl.print ', view: true' if view_match
|
57
|
+
tbl.print ', materialized: true' if view_match && view_match[1].presence
|
58
|
+
tbl.print ", to: \"#{view_match[2]}\"" if view_match && view_match[2].presence
|
55
59
|
end
|
56
60
|
|
57
61
|
if (id = columns.detect { |c| c.name == 'id' })
|
@@ -75,10 +79,10 @@ module ClickhouseActiverecord
|
|
75
79
|
tbl.puts ", force: :cascade do |t|"
|
76
80
|
|
77
81
|
# then dump all non-primary key columns
|
78
|
-
if simple || !
|
82
|
+
if simple || !view_match
|
79
83
|
columns.each do |column|
|
80
84
|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
|
81
|
-
next if column.name == pk
|
85
|
+
next if column.name == pk && column.name == "id"
|
82
86
|
type, colspec = column_spec(column)
|
83
87
|
name = column.name =~ (/\./) ? "\"`#{column.name}`\"" : column.name.inspect
|
84
88
|
tbl.print " t.#{type} #{name}"
|
@@ -108,11 +112,23 @@ module ClickhouseActiverecord
|
|
108
112
|
end
|
109
113
|
end
|
110
114
|
|
115
|
+
def column_spec_for_primary_key(column)
|
116
|
+
spec = super
|
117
|
+
|
118
|
+
id = ActiveRecord::ConnectionAdapters::ClickhouseAdapter::NATIVE_DATABASE_TYPES.invert[{name: column.sql_type.gsub(/\(\d+\)/, "")}]
|
119
|
+
spec[:id] = id.inspect if id.present?
|
120
|
+
|
121
|
+
spec.except!(:limit, :unsigned) # This can be removed at some date, it is only here to clean up existing schemas which have dumped these values already
|
122
|
+
end
|
123
|
+
|
111
124
|
def function(function, stream)
|
112
125
|
stream.puts " # FUNCTION: #{function}"
|
113
126
|
sql = @connection.show_create_function(function)
|
114
|
-
|
115
|
-
|
127
|
+
if sql
|
128
|
+
stream.puts " # SQL: #{sql}"
|
129
|
+
stream.puts " create_function \"#{function}\", \"#{sql.gsub(/^CREATE FUNCTION (.*?) AS/, '').strip}\", force: true"
|
130
|
+
stream.puts
|
131
|
+
end
|
116
132
|
end
|
117
133
|
|
118
134
|
def format_options(options)
|
@@ -141,23 +157,32 @@ module ClickhouseActiverecord
|
|
141
157
|
end
|
142
158
|
|
143
159
|
def schema_array(column)
|
144
|
-
(column.sql_type =~ /Array
|
160
|
+
(column.sql_type =~ /Array\(/).nil? ? nil : true
|
145
161
|
end
|
146
162
|
|
147
163
|
def schema_map(column)
|
148
|
-
|
164
|
+
if column.sql_type =~ /Map\(([^,]+),\s*(Array)\)/
|
165
|
+
return :array
|
166
|
+
end
|
167
|
+
|
168
|
+
(column.sql_type =~ /Map\(/).nil? ? nil : true
|
149
169
|
end
|
150
170
|
|
151
171
|
def schema_low_cardinality(column)
|
152
|
-
(column.sql_type =~ /LowCardinality
|
172
|
+
(column.sql_type =~ /LowCardinality\(/).nil? ? nil : true
|
153
173
|
end
|
154
174
|
|
175
|
+
# @param [ActiveRecord::ConnectionAdapters::Clickhouse::Column] column
|
155
176
|
def prepare_column_options(column)
|
156
177
|
spec = {}
|
157
178
|
spec[:unsigned] = schema_unsigned(column)
|
158
179
|
spec[:array] = schema_array(column)
|
159
180
|
spec[:map] = schema_map(column)
|
181
|
+
if spec[:map] == :array
|
182
|
+
spec[:array] = nil
|
183
|
+
end
|
160
184
|
spec[:low_cardinality] = schema_low_cardinality(column)
|
185
|
+
spec[:codec] = column.codec.inspect if column.codec
|
161
186
|
spec.merge(super).compact
|
162
187
|
end
|
163
188
|
|
@@ -47,12 +47,12 @@ module ClickhouseActiverecord
|
|
47
47
|
tables.sort_by! {|table| table.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : 0}
|
48
48
|
|
49
49
|
# get all functions
|
50
|
-
functions = connection.execute("SELECT create_query FROM system.functions WHERE origin = 'SQLUserDefined'")['data'].flatten
|
50
|
+
functions = connection.execute("SELECT create_query FROM system.functions WHERE origin = 'SQLUserDefined' ORDER BY name")['data'].flatten
|
51
51
|
|
52
52
|
# put to file
|
53
53
|
File.open(args.first, 'w:utf-8') do |file|
|
54
54
|
functions.each do |function|
|
55
|
-
file.puts function + ";\n\n"
|
55
|
+
file.puts function.gsub('\\n', "\n") + ";\n\n"
|
56
56
|
end
|
57
57
|
|
58
58
|
tables.each do |table|
|
@@ -67,6 +67,8 @@ module ClickhouseActiverecord
|
|
67
67
|
next
|
68
68
|
elsif sql =~ /^INSERT INTO/
|
69
69
|
connection.do_execute(sql, nil, format: nil)
|
70
|
+
elsif sql =~ /^CREATE .*?FUNCTION/
|
71
|
+
connection.do_execute(sql, nil, format: nil)
|
70
72
|
else
|
71
73
|
connection.execute(sql)
|
72
74
|
end
|
@@ -47,6 +47,33 @@ module CoreExtensions
|
|
47
47
|
self
|
48
48
|
end
|
49
49
|
|
50
|
+
# GROUPING SETS allows you to specify multiple groupings in the GROUP BY clause.
|
51
|
+
# Whereas GROUP BY CUBE generates all possible groupings, GROUP BY GROUPING SETS generates only the specified groupings.
|
52
|
+
# For example:
|
53
|
+
#
|
54
|
+
# users = User.group_by_grouping_sets([], [:name], [:name, :age]).select(:name, :age, 'count(*)')
|
55
|
+
# # SELECT name, age, count(*) FROM users GROUP BY GROUPING SETS ( (), (name), (name, age) )
|
56
|
+
#
|
57
|
+
# which is generally equivalent to:
|
58
|
+
# # SELECT NULL, NULL, count(*) FROM users
|
59
|
+
# # UNION ALL
|
60
|
+
# # SELECT name, NULL, count(*) FROM users GROUP BY name
|
61
|
+
# # UNION ALL
|
62
|
+
# # SELECT name, age, count(*) FROM users GROUP BY name, age
|
63
|
+
#
|
64
|
+
# Raises <tt>ArgumentError</tt> if no grouping sets are specified are provided.
|
65
|
+
def group_by_grouping_sets(*grouping_sets)
|
66
|
+
raise ArgumentError, 'The method .group_by_grouping_sets() must contain arguments.' if grouping_sets.blank?
|
67
|
+
|
68
|
+
spawn.group_by_grouping_sets!(*grouping_sets)
|
69
|
+
end
|
70
|
+
|
71
|
+
def group_by_grouping_sets!(*grouping_sets) # :nodoc:
|
72
|
+
grouping_sets = grouping_sets.map { |set| arel_columns(set) }
|
73
|
+
self.group_values += [::Arel::Nodes::GroupingSets.new(grouping_sets)]
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
50
77
|
# The USING clause specifies one or more columns to join, which establishes the equality of these columns. For example:
|
51
78
|
#
|
52
79
|
# users = User.joins(:joins).using(:event_name, :date)
|
@@ -81,6 +108,24 @@ module CoreExtensions
|
|
81
108
|
self
|
82
109
|
end
|
83
110
|
|
111
|
+
# The LIMIT BY clause permit to improve deduplication based on a unique key, it has better performances than
|
112
|
+
# the GROUP BY clause
|
113
|
+
#
|
114
|
+
# users = User.limit_by(1, id)
|
115
|
+
# # SELECT users.* FROM users LIMIT 1 BY id
|
116
|
+
#
|
117
|
+
# An <tt>ActiveRecord::ActiveRecordError</tt> will be reaised if database is not Clickhouse.
|
118
|
+
# @param [Array] opts
|
119
|
+
def limit_by(*opts)
|
120
|
+
spawn.limit_by!(*opts)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param [Array] opts
|
124
|
+
def limit_by!(*opts)
|
125
|
+
@values[:limit_by] = *opts
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
84
129
|
private
|
85
130
|
|
86
131
|
def check_command(cmd)
|
@@ -95,6 +140,7 @@ module CoreExtensions
|
|
95
140
|
end
|
96
141
|
|
97
142
|
arel.final! if @values[:final].present?
|
143
|
+
arel.limit_by(*@values[:limit_by]) if @values[:limit_by].present?
|
98
144
|
arel.settings(@values[:settings]) if @values[:settings].present?
|
99
145
|
arel.using(@values[:using]) if @values[:using].present?
|
100
146
|
arel.windows(@values[:windows]) if @values[:windows].present?
|
@@ -2,15 +2,18 @@ module CoreExtensions
|
|
2
2
|
module Arel # :nodoc: all
|
3
3
|
module Nodes
|
4
4
|
module SelectStatement
|
5
|
-
attr_accessor :settings
|
5
|
+
attr_accessor :limit_by, :settings
|
6
6
|
|
7
7
|
def initialize(relation = nil)
|
8
8
|
super
|
9
|
+
@limit_by = nil
|
9
10
|
@settings = nil
|
10
11
|
end
|
11
12
|
|
12
13
|
def eql?(other)
|
13
|
-
super &&
|
14
|
+
super &&
|
15
|
+
limit_by == other.limit_by &&
|
16
|
+
settings == other.settings
|
14
17
|
end
|
15
18
|
end
|
16
19
|
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.1
|
4
|
+
version: 1.2.1
|
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-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- bin/console
|
107
107
|
- bin/setup
|
108
108
|
- clickhouse-activerecord.gemspec
|
109
|
+
- lib/active_record/connection_adapters/clickhouse/column.rb
|
109
110
|
- lib/active_record/connection_adapters/clickhouse/oid/array.rb
|
110
111
|
- lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
|
111
112
|
- lib/active_record/connection_adapters/clickhouse/oid/date.rb
|
@@ -114,10 +115,12 @@ files:
|
|
114
115
|
- lib/active_record/connection_adapters/clickhouse/oid/uuid.rb
|
115
116
|
- lib/active_record/connection_adapters/clickhouse/quoting.rb
|
116
117
|
- lib/active_record/connection_adapters/clickhouse/schema_creation.rb
|
117
|
-
- lib/active_record/connection_adapters/clickhouse/schema_definitions.rb
|
118
118
|
- lib/active_record/connection_adapters/clickhouse/schema_statements.rb
|
119
|
+
- lib/active_record/connection_adapters/clickhouse/table_definition.rb
|
119
120
|
- lib/active_record/connection_adapters/clickhouse_adapter.rb
|
120
121
|
- lib/arel/nodes/final.rb
|
122
|
+
- lib/arel/nodes/grouping_sets.rb
|
123
|
+
- lib/arel/nodes/limit_by.rb
|
121
124
|
- lib/arel/nodes/settings.rb
|
122
125
|
- lib/arel/nodes/using.rb
|
123
126
|
- lib/arel/visitors/clickhouse.rb
|