clickhouse-activerecord 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/testing.yml +3 -3
- data/README.md +2 -1
- data/lib/active_record/connection_adapters/clickhouse/column.rb +21 -0
- data/lib/active_record/connection_adapters/clickhouse/oid/map.rb +4 -0
- data/lib/active_record/connection_adapters/clickhouse/schema_creation.rb +7 -1
- data/lib/active_record/connection_adapters/clickhouse/schema_statements.rb +6 -2
- data/lib/active_record/connection_adapters/clickhouse/{schema_definitions.rb → table_definition.rb} +6 -1
- data/lib/active_record/connection_adapters/clickhouse_adapter.rb +12 -11
- 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 +27 -19
- data/lib/clickhouse-activerecord/tasks.rb +2 -0
- 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: c3301f161ab9a3c94507a1d3a989e32710628746eeeac895966e64dd25579dd4
|
4
|
+
data.tar.gz: 2ab20aabc31a593987fb8905b5c58f023025a24cf447d0d922644ebbcff63397
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
-
|
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
|
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]
|
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
|
|
@@ -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
|
-
|
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,20 +50,16 @@ module ClickhouseActiverecord
|
|
50
50
|
|
51
51
|
unless simple
|
52
52
|
# Add materialize flag
|
53
|
-
tbl.print ', view: true' if
|
54
|
-
tbl.print ', materialized: true' if
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
pkcolspec = column_spec_for_primary_key(pkcol)
|
62
|
-
if pkcolspec.present?
|
63
|
-
tbl.print ", #{format_colspec(pkcolspec)}"
|
58
|
+
if (id = columns.detect { |c| c.name == 'id' })
|
59
|
+
spec = column_spec_for_primary_key(id)
|
60
|
+
if spec.present?
|
61
|
+
tbl.print ", #{format_colspec(spec)}"
|
64
62
|
end
|
65
|
-
when Array
|
66
|
-
tbl.print ", primary_key: #{pk.inspect}"
|
67
63
|
else
|
68
64
|
tbl.print ", id: false"
|
69
65
|
end
|
@@ -80,10 +76,10 @@ module ClickhouseActiverecord
|
|
80
76
|
tbl.puts ", force: :cascade do |t|"
|
81
77
|
|
82
78
|
# then dump all non-primary key columns
|
83
|
-
if simple || !
|
79
|
+
if simple || !view_match
|
84
80
|
columns.each do |column|
|
85
81
|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
|
86
|
-
next if column.name == pk
|
82
|
+
next if column.name == pk && column.name == "id"
|
87
83
|
type, colspec = column_spec(column)
|
88
84
|
name = column.name =~ (/\./) ? "\"`#{column.name}`\"" : column.name.inspect
|
89
85
|
tbl.print " t.#{type} #{name}"
|
@@ -116,8 +112,11 @@ module ClickhouseActiverecord
|
|
116
112
|
def function(function, stream)
|
117
113
|
stream.puts " # FUNCTION: #{function}"
|
118
114
|
sql = @connection.show_create_function(function)
|
119
|
-
|
120
|
-
|
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
|
121
120
|
end
|
122
121
|
|
123
122
|
def format_options(options)
|
@@ -146,23 +145,32 @@ module ClickhouseActiverecord
|
|
146
145
|
end
|
147
146
|
|
148
147
|
def schema_array(column)
|
149
|
-
(column.sql_type =~ /Array
|
148
|
+
(column.sql_type =~ /Array\(/).nil? ? nil : true
|
150
149
|
end
|
151
150
|
|
152
151
|
def schema_map(column)
|
153
|
-
|
152
|
+
if column.sql_type =~ /Map\(([^,]+),\s*(Array)\)/
|
153
|
+
return :array
|
154
|
+
end
|
155
|
+
|
156
|
+
(column.sql_type =~ /Map\(/).nil? ? nil : true
|
154
157
|
end
|
155
158
|
|
156
159
|
def schema_low_cardinality(column)
|
157
|
-
(column.sql_type =~ /LowCardinality
|
160
|
+
(column.sql_type =~ /LowCardinality\(/).nil? ? nil : true
|
158
161
|
end
|
159
162
|
|
163
|
+
# @param [ActiveRecord::ConnectionAdapters::Clickhouse::Column] column
|
160
164
|
def prepare_column_options(column)
|
161
165
|
spec = {}
|
162
166
|
spec[:unsigned] = schema_unsigned(column)
|
163
167
|
spec[:array] = schema_array(column)
|
164
168
|
spec[:map] = schema_map(column)
|
169
|
+
if spec[:map] == :array
|
170
|
+
spec[:array] = nil
|
171
|
+
end
|
165
172
|
spec[:low_cardinality] = schema_low_cardinality(column)
|
173
|
+
spec[:codec] = column.codec.inspect if column.codec
|
166
174
|
spec.merge(super).compact
|
167
175
|
end
|
168
176
|
|
@@ -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.
|
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-
|
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
|