clickhouse-activerecord 1.1.3 → 1.2.1
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/.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
|