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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 419c50a4b99b183f729939034e8f6b671bf899fb0ef93fc97f105798fa437aa1
4
- data.tar.gz: 7508a2ed443d64565ca6b1f5af48bf076cf40d2eae91fd3c3187cae05371a864
3
+ metadata.gz: b40a04fe93423fd3469b65a66429ecb41e4c5c25440777c69617e69081e578b4
4
+ data.tar.gz: 43586cb853dec4d3453a6b6938158e5f1197504678c2e1aa824b8521a6a1c628
5
5
  SHA512:
6
- metadata.gz: 8e8607fa219f6e17e22a3029ba564e6d3d6d2bd39cf86f639d277a6e83fe2ffb1a47e93fceb8d20c4de6a7e9679f8a15b40ee744bfa445be14e6e0693dde4a0e
7
- data.tar.gz: 5e58b79aab91a64387dc74821a75710c70738791227084a3a6ce29e72827a40e0458d48643a99c14b6d2dc6c94bbed8672d31051d7501084aa3a7e0bc27ef6a1
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.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
@@ -7,15 +7,16 @@ module ActiveRecord
7
7
  class Map < Type::Value # :nodoc:
8
8
 
9
9
  def initialize(sql_type)
10
- @subtype = case sql_type
11
- when /U?Int\d+/
12
- :integer
13
- when /DateTime/
14
- :datetime
15
- when /Date/
16
- :date
17
- else
18
- :string
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
- if res.body.to_s.include?("DB::Exception")
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
- ClickhouseColumn.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), default_function)
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
@@ -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!, :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
 
@@ -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
- sorted_tables = @connection.tables.sort {|a,b| @connection.show_create_table(a).match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : a <=> b }
24
- sorted_tables.each do |table_name|
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
- match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/)
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 match
54
- tbl.print ', materialized: true' if match && match[1].presence
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 || !match
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
- stream.puts " # SQL: #{sql}" if sql
115
- stream.puts " create_function \"#{function}\", \"#{sql.gsub(/^CREATE FUNCTION (.*?) AS/, '').strip}\"" if sql
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?\(/).nil? ? nil : true
160
+ (column.sql_type =~ /Array\(/).nil? ? nil : true
145
161
  end
146
162
 
147
163
  def schema_map(column)
148
- (column.sql_type =~ /Map?\(/).nil? ? nil : true
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?\(/).nil? ? nil : true
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
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '1.1.3'
2
+ VERSION = '1.2.1'
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.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-09-27 00:00:00.000000000 Z
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