clickhouse-activerecord 1.2.0 → 1.3.0

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: c3301f161ab9a3c94507a1d3a989e32710628746eeeac895966e64dd25579dd4
4
- data.tar.gz: 2ab20aabc31a593987fb8905b5c58f023025a24cf447d0d922644ebbcff63397
3
+ metadata.gz: 8e47b553f60d06d69a8e1f7c70bbc495c32502f0ea75f3ede9c8ac2bafb063ee
4
+ data.tar.gz: 42ec02c29478ae577fda1f1a726d003e925b2c36faa02fd6a92797a4e05f960c
5
5
  SHA512:
6
- metadata.gz: f2d35a1489031909c0a2b8c2f8feb5624b93854bfbcabd9e39e006874033d0b534d568e72deabb795fcbeb1c0a3416966df8168b6f10d1ef64a3bb36d96b838f
7
- data.tar.gz: 9c6be79ff71e1207cd2b162e909db8b7db8a2276ce17351b2b2fc38097dc47f8c4dd8386cfacdc37bd1ccd3cfc28072dc45bd09b155737ef28b63a3a55f35def
6
+ metadata.gz: 405f20e2c53352ae702b4b462165ccbf5306da2e72733d5c8f80588ba5392637e6b8d2c95d6d61cc9f777d0fc49e48a3578ee6db2fdb292668cb4ec0c52d0471
7
+ data.tar.gz: 8b3bebc0c3672112c26889555d1eb0b280cbc331758cbf1d5d1733273983e10178929bec76fda6793a654d1ff2c0048b44a58eca533f3ec0a2e8481e7d928314
@@ -28,13 +28,15 @@ jobs:
28
28
  rails: 7.1.3
29
29
  - ruby: 3.2
30
30
  rails: 7.2.1
31
- clickhouse: [ '22.1', '24.6' ]
31
+ - ruby: 3.2
32
+ rails: 8.0.1
33
+ clickhouse: [ '22.1', '24.9' ]
32
34
 
33
35
  steps:
34
36
  - uses: actions/checkout@v4
35
37
 
36
38
  - name: Start ClickHouse ${{ matrix.clickhouse }}
37
- uses: isbang/compose-action@v1.5.1
39
+ uses: hoverkraft-tech/compose-action@v2.1.0
38
40
  env:
39
41
  CLICKHOUSE_VERSION: ${{ matrix.clickhouse }}
40
42
  with:
@@ -72,14 +74,16 @@ jobs:
72
74
  - ruby: 3.2
73
75
  rails: 7.1.3
74
76
  - ruby: 3.2
75
- rails: 7.2.0
76
- clickhouse: [ '22.1', '24.6' ]
77
+ rails: 7.2.1
78
+ - ruby: 3.2
79
+ rails: 8.0.1
80
+ clickhouse: [ '22.1', '24.9' ]
77
81
 
78
82
  steps:
79
83
  - uses: actions/checkout@v4
80
84
 
81
85
  - name: Start ClickHouse Cluster ${{ matrix.clickhouse }}
82
- uses: isbang/compose-action@v1.5.1
86
+ uses: hoverkraft-tech/compose-action@v2.1.0
83
87
  env:
84
88
  CLICKHOUSE_VERSION: ${{ matrix.clickhouse }}
85
89
  with:
data/README.md CHANGED
@@ -184,7 +184,11 @@ Action.settings(optimize_read_in_order: 1).where(date: Date.current).limit(10)
184
184
 
185
185
  User.joins(:actions).using(:group_id)
186
186
  # Clickhouse User Load (10.3ms) SELECT users.* FROM users INNER JOIN actions USING group_id
187
- #=> #<ActiveRecord::Relation [#<Action *** >]>
187
+ #=> #<ActiveRecord::Relation [#<User *** >]>
188
+
189
+ User.window('x', order: 'date', partition: 'name', rows: 'UNBOUNDED PRECEDING').select('sum(value) OVER x')
190
+ # SELECT sum(value) OVER x FROM users WINDOW x AS (PARTITION BY name ORDER BY date ROWS UNBOUNDED PRECEDING)
191
+ #=> #<ActiveRecord::Relation [#<User *** >]>
188
192
  ```
189
193
 
190
194
 
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ['lib']
25
25
 
26
26
  spec.add_runtime_dependency 'bundler', '>= 1.13.4'
27
- spec.add_runtime_dependency 'activerecord', '~> 7.1'
27
+ spec.add_runtime_dependency 'activerecord', '>= 7.1', '< 9.0'
28
28
 
29
29
  spec.add_development_dependency 'rake', '~> 13.0'
30
30
  spec.add_development_dependency 'rspec', '~> 3.4'
@@ -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
 
@@ -65,6 +66,19 @@ module ActiveRecord
65
66
  end
66
67
  end
67
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
+
68
82
  end
69
83
  end
70
84
  end
@@ -72,8 +72,14 @@ module ActiveRecord
72
72
  result['data'].flatten
73
73
  end
74
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
+
75
81
  def functions
76
- 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")
77
83
  return [] if result.nil?
78
84
  result['data'].flatten
79
85
  end
@@ -172,10 +178,12 @@ module ActiveRecord
172
178
  def request(sql, format = nil, settings = {})
173
179
  formatted_sql = apply_format(sql, format)
174
180
  request_params = @connection_config || {}
175
- @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, {
176
- 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}",
177
- 'Content-Type' => 'application/x-www-form-urlencoded',
178
- })
181
+ @lock.synchronize do
182
+ @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, {
183
+ 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}",
184
+ 'Content-Type' => 'application/x-www-form-urlencoded',
185
+ })
186
+ end
179
187
  end
180
188
 
181
189
  def apply_format(sql, format)
@@ -102,7 +102,7 @@ module ActiveRecord
102
102
  private
103
103
 
104
104
  def valid_column_definition_options
105
- super + [:array, :low_cardinality, :fixed_string, :value, :type, :map, :codec]
105
+ super + [:array, :low_cardinality, :fixed_string, :value, :type, :map, :codec, :unsigned]
106
106
  end
107
107
  end
108
108
 
@@ -191,6 +191,8 @@ module ActiveRecord
191
191
  nil
192
192
  when /(Nullable)?\(?U?Int64\)?/
193
193
  8
194
+ when /(Nullable)?\(?U?Int128\)?/
195
+ 16
194
196
  else
195
197
  super
196
198
  end
@@ -298,9 +300,11 @@ module ActiveRecord
298
300
  end
299
301
 
300
302
  # @param [String] table
303
+ # @option [Boolean] single_line
301
304
  # @return [String]
302
- def show_create_table(table)
303
- do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first).gsub(/[\n\s]+/m, ' ').gsub("#{@config[:database]}.", "")
305
+ def show_create_table(table, single_line: true)
306
+ sql = do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first)
307
+ single_line ? sql.squish : sql
304
308
  end
305
309
 
306
310
  # Create a new ClickHouse database.
@@ -312,7 +316,7 @@ module ActiveRecord
312
316
  end
313
317
  end
314
318
 
315
- def create_view(table_name, **options)
319
+ def create_view(table_name, request_settings: {}, **options)
316
320
  options.merge!(view: true)
317
321
  options = apply_replica(table_name, options)
318
322
  td = create_table_definition(apply_cluster(table_name), **options)
@@ -322,10 +326,10 @@ module ActiveRecord
322
326
  drop_table(table_name, options.merge(if_exists: true))
323
327
  end
324
328
 
325
- do_execute(schema_creation.accept(td), format: nil)
329
+ do_execute(schema_creation.accept(td), format: nil, settings: request_settings)
326
330
  end
327
331
 
328
- def create_table(table_name, **options, &block)
332
+ def create_table(table_name, request_settings: {}, **options, &block)
329
333
  options = apply_replica(table_name, options)
330
334
  td = create_table_definition(apply_cluster(table_name), **options)
331
335
  block.call td if block_given?
@@ -339,7 +343,7 @@ module ActiveRecord
339
343
  drop_table(table_name, options.merge(if_exists: true))
340
344
  end
341
345
 
342
- do_execute(schema_creation.accept(td), format: nil)
346
+ do_execute(schema_creation.accept(td), format: nil, settings: request_settings)
343
347
 
344
348
  if options[:with_distributed]
345
349
  distributed_table_name = options.delete(:with_distributed)
@@ -4,6 +4,11 @@ module Arel
4
4
  module Visitors
5
5
  class Clickhouse < ::Arel::Visitors::ToSql
6
6
 
7
+ def compile(node, collector = Arel::Collectors::SQLString.new)
8
+ @delete_or_update = false
9
+ super
10
+ end
11
+
7
12
  def aggregate(name, o, collector)
8
13
  # replacing function name for materialized view
9
14
  if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation&.is_view
@@ -16,12 +21,11 @@ module Arel
16
21
  # https://clickhouse.com/docs/en/sql-reference/statements/delete
17
22
  # DELETE and UPDATE in ClickHouse working only without table name
18
23
  def visit_Arel_Attributes_Attribute(o, collector)
19
- if collector.value.is_a?(String)
20
- collector << quote_table_name(o.relation.table_alias || o.relation.name) << '.' unless collector.value.start_with?('DELETE FROM ') || collector.value.include?(' UPDATE ')
21
- collector << quote_column_name(o.name)
22
- else
23
- super
24
+ unless @delete_or_update
25
+ join_name = o.relation.table_alias || o.relation.name
26
+ collector << quote_table_name(join_name) << '.'
24
27
  end
28
+ collector << quote_column_name(o.name)
25
29
  end
26
30
 
27
31
  def visit_Arel_Nodes_SelectOptions(o, collector)
@@ -30,6 +34,7 @@ module Arel
30
34
  end
31
35
 
32
36
  def visit_Arel_Nodes_UpdateStatement(o, collector)
37
+ @delete_or_update = true
33
38
  o = prepare_update_statement(o)
34
39
 
35
40
  collector << 'ALTER TABLE '
@@ -40,6 +45,11 @@ module Arel
40
45
  maybe_visit o.limit, collector
41
46
  end
42
47
 
48
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
49
+ @delete_or_update = true
50
+ super
51
+ end
52
+
43
53
  def visit_Arel_Nodes_Final(o, collector)
44
54
  visit o.expr, collector
45
55
  collector << ' FINAL'
@@ -64,7 +74,7 @@ module Arel
64
74
  collector
65
75
  end
66
76
 
67
- def visit_Arel_Nodes_Using o, collector
77
+ def visit_Arel_Nodes_Using(o, collector)
68
78
  collector << "USING "
69
79
  visit o.expr, collector
70
80
  collector
@@ -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
@@ -109,6 +112,15 @@ module ClickhouseActiverecord
109
112
  end
110
113
  end
111
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
+
112
124
  def function(function, stream)
113
125
  stream.puts " # FUNCTION: #{function}"
114
126
  sql = @connection.show_create_function(function)
@@ -40,19 +40,19 @@ module ClickhouseActiverecord
40
40
  # get all tables
41
41
  tables = connection.execute("SHOW TABLES FROM #{@configuration.database} WHERE name NOT LIKE '.inner_id.%'")['data'].flatten.map do |table|
42
42
  next if %w[schema_migrations ar_internal_metadata].include?(table)
43
- connection.show_create_table(table).gsub("#{@configuration.database}.", '')
43
+ connection.show_create_table(table, single_line: false).gsub("#{@configuration.database}.", '')
44
44
  end.compact
45
45
 
46
46
  # sort view to last
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|
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '1.2.0'
2
+ VERSION = '1.3.0'
3
3
  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.2.0
4
+ version: 1.3.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-10-23 00:00:00.000000000 Z
11
+ date: 2025-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '7.1'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '9.0'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '7.1'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '9.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: rake
43
49
  requirement: !ruby/object:Gem::Requirement