clickhouse-activerecord 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 419c50a4b99b183f729939034e8f6b671bf899fb0ef93fc97f105798fa437aa1
4
- data.tar.gz: 7508a2ed443d64565ca6b1f5af48bf076cf40d2eae91fd3c3187cae05371a864
3
+ metadata.gz: c3301f161ab9a3c94507a1d3a989e32710628746eeeac895966e64dd25579dd4
4
+ data.tar.gz: 2ab20aabc31a593987fb8905b5c58f023025a24cf447d0d922644ebbcff63397
5
5
  SHA512:
6
- metadata.gz: 8e8607fa219f6e17e22a3029ba564e6d3d6d2bd39cf86f639d277a6e83fe2ffb1a47e93fceb8d20c4de6a7e9679f8a15b40ee744bfa445be14e6e0693dde4a0e
7
- data.tar.gz: 5e58b79aab91a64387dc74821a75710c70738791227084a3a6ce29e72827a40e0458d48643a99c14b6d2dc6c94bbed8672d31051d7501084aa3a7e0bc27ef6a1
6
+ metadata.gz: f2d35a1489031909c0a2b8c2f8feb5624b93854bfbcabd9e39e006874033d0b534d568e72deabb795fcbeb1c0a3416966df8168b6f10d1ef64a3bb36d96b838f
7
+ data.tar.gz: 9c6be79ff71e1207cd2b162e909db8b7db8a2276ce17351b2b2fc38097dc47f8c4dd8386cfacdc37bd1ccd3cfc28072dc45bd09b155737ef28b63a3a55f35def
@@ -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.0
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
@@ -26,6 +26,8 @@ module ActiveRecord
26
26
  def deserialize(value)
27
27
  if value.is_a?(::Hash)
28
28
  value.map { |k, item| [k.to_s, deserialize(item)] }.to_h
29
+ elsif value.is_a?(::Array)
30
+ value.map { |item| deserialize(item) }
29
31
  else
30
32
  return value if value.nil?
31
33
  case @subtype
@@ -44,6 +46,8 @@ module ActiveRecord
44
46
  def serialize(value)
45
47
  if value.is_a?(::Hash)
46
48
  value.map { |k, item| [k.to_s, serialize(item)] }.to_h
49
+ elsif value.is_a?(::Array)
50
+ value.map { |item| serialize(item) }
47
51
  else
48
52
  return value if value.nil?
49
53
  case @subtype
@@ -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
@@ -183,7 +185,9 @@ module ActiveRecord
183
185
  def process_response(res, format, sql = nil)
184
186
  case res.code.to_i
185
187
  when 200
186
- if res.body.to_s.include?("DB::Exception")
188
+ body = res.body
189
+
190
+ if body.include?("DB::Exception") && body.match?(DB_EXCEPTION_REGEXP)
187
191
  raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}#{sql ? "\nQuery: #{sql}" : ''}"
188
192
  else
189
193
  format_body_response(res.body, format)
@@ -221,7 +225,7 @@ module ActiveRecord
221
225
  default_value = extract_value_from_default(field[3], field[2])
222
226
  default_function = extract_default_function(field[3])
223
227
  default_value = lookup_cast_type(sql_type).cast(default_value)
224
- ClickhouseColumn.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), default_function)
228
+ Clickhouse::Column.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), default_function, codec: field[5].presence)
225
229
  end
226
230
 
227
231
  protected
@@ -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]
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!, :settings, :settings!, :window, :window!, to: :all
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
 
@@ -351,8 +352,8 @@ module ActiveRecord
351
352
  end
352
353
  end
353
354
 
354
- def create_function(name, body)
355
- fd = "CREATE FUNCTION #{apply_cluster(quote_table_name(name))} AS #{body}"
355
+ def create_function(name, body, **options)
356
+ fd = "CREATE#{' OR REPLACE' if options[:force]} FUNCTION #{apply_cluster(quote_table_name(name))} AS #{body}"
356
357
  do_execute(fd, format: nil)
357
358
  end
358
359
 
@@ -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
@@ -35,7 +35,7 @@ module ClickhouseActiverecord
35
35
  # super(table.gsub(/^\.inner\./, ''), stream)
36
36
 
37
37
  # detect view table
38
- match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/)
38
+ view_match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW\s+\S+\s+(?:TO (\S+))?/)
39
39
  end
40
40
 
41
41
  # Copy from original dumper
@@ -50,8 +50,9 @@ module ClickhouseActiverecord
50
50
 
51
51
  unless simple
52
52
  # Add materialize flag
53
- tbl.print ', view: true' if match
54
- tbl.print ', materialized: true' if match && match[1].presence
53
+ tbl.print ', view: true' if view_match
54
+ tbl.print ', materialized: true' if view_match && view_match[1].presence
55
+ tbl.print ", to: \"#{view_match[2]}\"" if view_match && view_match[2].presence
55
56
  end
56
57
 
57
58
  if (id = columns.detect { |c| c.name == 'id' })
@@ -75,10 +76,10 @@ module ClickhouseActiverecord
75
76
  tbl.puts ", force: :cascade do |t|"
76
77
 
77
78
  # then dump all non-primary key columns
78
- if simple || !match
79
+ if simple || !view_match
79
80
  columns.each do |column|
80
81
  raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
81
- next if column.name == pk
82
+ next if column.name == pk && column.name == "id"
82
83
  type, colspec = column_spec(column)
83
84
  name = column.name =~ (/\./) ? "\"`#{column.name}`\"" : column.name.inspect
84
85
  tbl.print " t.#{type} #{name}"
@@ -111,8 +112,11 @@ module ClickhouseActiverecord
111
112
  def function(function, stream)
112
113
  stream.puts " # FUNCTION: #{function}"
113
114
  sql = @connection.show_create_function(function)
114
- stream.puts " # SQL: #{sql}" if sql
115
- stream.puts " create_function \"#{function}\", \"#{sql.gsub(/^CREATE FUNCTION (.*?) AS/, '').strip}\"" if sql
115
+ if sql
116
+ stream.puts " # SQL: #{sql}"
117
+ stream.puts " create_function \"#{function}\", \"#{sql.gsub(/^CREATE FUNCTION (.*?) AS/, '').strip}\", force: true"
118
+ stream.puts
119
+ end
116
120
  end
117
121
 
118
122
  def format_options(options)
@@ -141,23 +145,32 @@ module ClickhouseActiverecord
141
145
  end
142
146
 
143
147
  def schema_array(column)
144
- (column.sql_type =~ /Array?\(/).nil? ? nil : true
148
+ (column.sql_type =~ /Array\(/).nil? ? nil : true
145
149
  end
146
150
 
147
151
  def schema_map(column)
148
- (column.sql_type =~ /Map?\(/).nil? ? nil : true
152
+ if column.sql_type =~ /Map\(([^,]+),\s*(Array)\)/
153
+ return :array
154
+ end
155
+
156
+ (column.sql_type =~ /Map\(/).nil? ? nil : true
149
157
  end
150
158
 
151
159
  def schema_low_cardinality(column)
152
- (column.sql_type =~ /LowCardinality?\(/).nil? ? nil : true
160
+ (column.sql_type =~ /LowCardinality\(/).nil? ? nil : true
153
161
  end
154
162
 
163
+ # @param [ActiveRecord::ConnectionAdapters::Clickhouse::Column] column
155
164
  def prepare_column_options(column)
156
165
  spec = {}
157
166
  spec[:unsigned] = schema_unsigned(column)
158
167
  spec[:array] = schema_array(column)
159
168
  spec[:map] = schema_map(column)
169
+ if spec[:map] == :array
170
+ spec[:array] = nil
171
+ end
160
172
  spec[:low_cardinality] = schema_low_cardinality(column)
173
+ spec[:codec] = column.codec.inspect if column.codec
161
174
  spec.merge(super).compact
162
175
  end
163
176
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '1.1.3'
2
+ VERSION = '1.2.0'
3
3
  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 && settings == other.settings
14
+ super &&
15
+ limit_by == other.limit_by &&
16
+ settings == other.settings
14
17
  end
15
18
  end
16
19
  end
@@ -29,6 +29,11 @@ module CoreExtensions
29
29
  @ctx.source.right.last.right = ::Arel::Nodes::Using.new(::Arel.sql(exprs.join(',')))
30
30
  self
31
31
  end
32
+
33
+ def limit_by(*exprs)
34
+ @ast.limit_by = ::Arel::Nodes::LimitBy.new(*exprs)
35
+ self
36
+ end
32
37
  end
33
38
  end
34
39
  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.3
4
+ version: 1.2.0
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-09-27 00:00:00.000000000 Z
11
+ date: 2024-10-23 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