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 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