clickhouse-activerecord 0.4.7 → 0.5.7

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: ed292c2d626374fcb26194a4ca28ca34d60f8c49105b7217bfc7654a5a1b08f0
4
- data.tar.gz: 97d4074d47e641e8202fd8796ea049f78bf76d86906ce08877f3eb96b236b116
3
+ metadata.gz: 79b43575e5eef93daa11e1a2aca2984f696cbef59021f55d1470b2dc0a5d1e3d
4
+ data.tar.gz: b79cccb85d8a07fa4dc533d7b7e9042eeaa2afead26214c075b3131e1843394b
5
5
  SHA512:
6
- metadata.gz: 23c4d834c3c3bbaa000b8bd9202064cbabf2c366a8984d1b2fd92dc51e4c6d2811301e39cd8d2b80141581dcedb8cb6647dcbe59d90eb0b0987afdd5f1b22efa
7
- data.tar.gz: 0ac017b6614a3072fdad422db66a36201d5e3a62269230f102fbe8b6c7085f48a139cd8c0753adab11ca53a25e91e6eb327298036ebc385eb26f302c4e5178f8
6
+ metadata.gz: 840b01cc1d5b88eee5e031fada23cfd35f7a8d948a35a767b2f5baf1596af665f28c56778e699960511004367b16ee3130be1f5bd2ee713a9a0d5053a9b58a37
7
+ data.tar.gz: 4e613a19c51b362a05634da06d9528809ed09f01ebec4eac9984656156e9b877bdc4ce321e24bea63ae4e49d85609fa9dda781db60159e60b6d6ee06e8a99a3e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ### Version 0.5.6 (Oct 25, 2021)
2
+
3
+ * Added auto creating service distributed tables and additional options for creating view [@ygreeek](https://github.com/ygreeek)
4
+ * Added default user agent
5
+
6
+ ### Version 0.5.3 (Sep 22, 2021)
7
+
8
+ * Fix replica cluster for a new syntax MergeTree
9
+ * Fix support rails 5.2 on alter table
10
+ * Support array type of column
11
+ * Support Rails 6.1.0 [@bdevel](https://github.com/bdevel)
12
+
13
+ ### Version 0.4.10 (Mar 10, 2021)
14
+
15
+ * Support ClickHouse 20.9+
16
+ * Fix schema create / dump
17
+ * Support all integer types through :limit and :unsigned [@bdevel](https://github.com/bdevel)
18
+
1
19
  ### Version 0.4.4 (Sep 23, 2020)
2
20
 
3
21
  * Full support migration and rollback database
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Clickhouse::Activerecord
2
2
 
3
3
  A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
4
- Support ClickHouse version from 19.14 LTS.
4
+ Support ClickHouse version from 20.9 LTS.
5
5
 
6
6
  ## Installation
7
7
 
@@ -35,6 +35,21 @@ default: &default
35
35
  replica_name: '{replica}' # replica macros name, optional for creating replicated tables
36
36
  ```
37
37
 
38
+ Alternatively if you wish to pass a custom `Net::HTTP` transport (or any other
39
+ object which supports a `.post()` function with the same parameters as
40
+ `Net::HTTP`'s), you can do this directly instead of specifying
41
+ `host`/`port`/`ssl`:
42
+
43
+ ```ruby
44
+ class ActionView < ActiveRecord::Base
45
+ establish_connection(
46
+ adapter: 'clickhouse',
47
+ database: 'database',
48
+ connection: Net::HTTP.start('http://example.org', 8123)
49
+ )
50
+ end
51
+ ```
52
+
38
53
  ## Usage in Rails 5
39
54
 
40
55
  Add your `database.yml` connection information with postfix `_clickhouse` for you environment:
@@ -162,6 +177,45 @@ ActionView.maximum(:date)
162
177
  #=> 'Wed, 29 Nov 2017'
163
178
  ```
164
179
 
180
+
181
+ ### Migration Data Types
182
+
183
+ Integer types are unsigned by default. Specify signed values with `:unsigned =>
184
+ false`. The default integer is `UInt32`
185
+
186
+ | Type (bit size) | Range | :limit (byte size) |
187
+ | :--- | :----: | ---: |
188
+ | Int8 | -128 to 127 | 1 |
189
+ | Int16 | -32768 to 32767 | 2 |
190
+ | Int32 | -2147483648 to 2,147,483,647 | 3,4 |
191
+ | Int64 | -9223372036854775808 to 9223372036854775807] | 5,6,7,8 |
192
+ | Int128 | ... | 9 - 15 |
193
+ | Int256 | ... | 16+ |
194
+ | UInt8 | 0 to 255 | 1 |
195
+ | UInt16 | 0 to 65,535 | 2 |
196
+ | UInt32 | 0 to 4,294,967,295 | 3,4 |
197
+ | UInt64 | 0 to 18446744073709551615 | 5,6,7,8 |
198
+ | UInt256 | 0 to ... | 8+ |
199
+ | Array | ... | ... |
200
+
201
+ Example:
202
+
203
+ ``` ruby
204
+ class CreateDataItems < ActiveRecord::Migration
205
+ def change
206
+ create_table "data_items", id: false, options: "VersionedCollapsingMergeTree(sign, version) PARTITION BY toYYYYMM(day) ORDER BY category", force: :cascade do |t|
207
+ t.date "day", null: false
208
+ t.string "category", null: false
209
+ t.integer "value_in", null: false
210
+ t.integer "sign", limit: 1, unsigned: false, default: -> { "CAST(1, 'Int8')" }, null: false
211
+ t.integer "version", limit: 8, default: -> { "CAST(toUnixTimestamp(now()), 'UInt64')" }, null: false
212
+ end
213
+ end
214
+ end
215
+
216
+ ```
217
+
218
+
165
219
  ### Using replica and cluster params in connection parameters
166
220
 
167
221
  ```yml
@@ -0,0 +1,27 @@
1
+ module CoreExtensions
2
+ module ActiveRecord
3
+ module Migration
4
+ module CommandRecorder
5
+ def create_table_with_distributed(*args, &block)
6
+ record(:create_table_with_distributed, args, &block)
7
+ end
8
+
9
+ def create_view(*args, &block)
10
+ record(:create_view, args, &block)
11
+ end
12
+
13
+ private
14
+
15
+ def invert_create_table_with_distributed(args)
16
+ table_name, options = args
17
+ [:drop_table_with_distributed, table_name, options]
18
+ end
19
+
20
+ def invert_create_view(args)
21
+ view_name, options = args
22
+ [:drop_table, view_name, options]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Clickhouse
6
+ module OID # :nodoc:
7
+ class Array < Type::Value # :nodoc:
8
+
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
19
+ end
20
+ end
21
+
22
+ def type
23
+ @subtype
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
+ begin
3
+ require "active_record/connection_adapters/deduplicable"
4
+ rescue LoadError => e
5
+ # Rails < 6.1 does not have this file in this location, ignore
6
+ end
7
+
8
+ require "active_record/connection_adapters/abstract/schema_creation"
2
9
 
3
10
  module ActiveRecord
4
11
  module ConnectionAdapters
5
12
  module Clickhouse
6
- class SchemaCreation < AbstractAdapter::SchemaCreation# :nodoc:
13
+ class SchemaCreation < ConnectionAdapters::SchemaCreation# :nodoc:
7
14
 
8
15
  def visit_AddColumnDefinition(o)
9
16
  sql = +"ADD COLUMN #{accept(o.column)}"
@@ -15,14 +22,23 @@ module ActiveRecord
15
22
  if options[:null] || options[:null].nil?
16
23
  sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
17
24
  end
25
+ if options[:array]
26
+ sql.gsub!(/\s+(.*)/, ' Array(\1)')
27
+ end
18
28
  sql.gsub!(/(\sString)\(\d+\)/, '\1')
19
29
  sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
20
30
  sql
21
31
  end
22
32
 
23
33
  def add_table_options!(create_sql, options)
24
- if options[:options].present?
25
- create_sql << " ENGINE = #{options[:options]}"
34
+ opts = options[:options]
35
+ if options.respond_to?(:options)
36
+ # rails 6.1
37
+ opts ||= options.options
38
+ end
39
+
40
+ if opts.present?
41
+ create_sql << " ENGINE = #{opts}"
26
42
  else
27
43
  create_sql << " ENGINE = Log()"
28
44
  end
@@ -30,17 +46,48 @@ module ActiveRecord
30
46
  create_sql
31
47
  end
32
48
 
49
+ def add_as_clause!(create_sql, options)
50
+ return unless options.as
51
+
52
+ assign_database_to_subquery!(options.as) if options.view
53
+ create_sql << " AS #{to_sql(options.as)}"
54
+ end
55
+
56
+ def assign_database_to_subquery!(subquery)
57
+ # If you do not specify a database explicitly, ClickHouse will use the "default" database.
58
+ return unless subquery
59
+
60
+ match = subquery.match(/(?<=from)[^.\w]+(?<database>\w+(?=\.))?(?<table_name>[.\w]+)/i)
61
+ return unless match
62
+ return if match[:database]
63
+
64
+ subquery[match.begin(:table_name)...match.end(:table_name)] =
65
+ "#{current_database}.#{match[:table_name].sub('.', '')}"
66
+ end
67
+
68
+ def add_to_clause!(create_sql, options)
69
+ # If you do not specify a database explicitly, ClickHouse will use the "default" database.
70
+ return unless options.to
71
+
72
+ match = options.to.match(/(?<database>.+(?=\.))?(?<table_name>.+)/i)
73
+ return unless match
74
+ return if match[:database]
75
+
76
+ create_sql << "TO #{current_database}.#{options.to.sub('.', '')} "
77
+ end
78
+
33
79
  def visit_TableDefinition(o)
34
80
  create_sql = +"CREATE#{table_modifier_in_create(o)} #{o.view ? "VIEW" : "TABLE"} "
35
81
  create_sql << "IF NOT EXISTS " if o.if_not_exists
36
82
  create_sql << "#{quote_table_name(o.name)} "
83
+ add_to_clause!(create_sql, o) if o.materialized
37
84
 
38
85
  statements = o.columns.map { |c| accept c }
39
86
  statements << accept(o.primary_keys) if o.primary_keys
40
-
41
87
  create_sql << "(#{statements.join(', ')})" if statements.present?
42
- add_table_options!(create_sql, table_options(o))
43
- create_sql << " AS #{to_sql(o.as)}" if o.as
88
+ # Attach options for only table or materialized view without TO section
89
+ add_table_options!(create_sql, o) if !o.view || o.view && o.materialized && !o.to
90
+ add_as_clause!(create_sql, o)
44
91
  create_sql
45
92
  end
46
93
 
@@ -68,6 +115,9 @@ module ActiveRecord
68
115
  change_column_sql
69
116
  end
70
117
 
118
+ def current_database
119
+ ActiveRecord::Base.connection_db_config.database
120
+ end
71
121
  end
72
122
  end
73
123
  end
@@ -5,7 +5,7 @@ module ActiveRecord
5
5
  module Clickhouse
6
6
  class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
7
7
 
8
- attr_reader :view, :materialized, :if_not_exists
8
+ attr_reader :view, :materialized, :if_not_exists, :to
9
9
 
10
10
  def initialize(
11
11
  conn,
@@ -17,6 +17,7 @@ module ActiveRecord
17
17
  comment: nil,
18
18
  view: false,
19
19
  materialized: false,
20
+ to: nil,
20
21
  **
21
22
  )
22
23
  @conn = conn
@@ -32,16 +33,35 @@ module ActiveRecord
32
33
  @comment = comment
33
34
  @view = view || materialized
34
35
  @materialized = materialized
36
+ @to = to
35
37
  end
36
38
 
37
39
  def integer(*args, **options)
38
- if options[:limit] == 8
39
- args.each { |name| column(name, :big_integer, options.except(:limit)) }
40
- else
41
- super
40
+ # default to unsigned
41
+ unsigned = options[:unsigned]
42
+ unsigned = true if unsigned.nil?
43
+
44
+ kind = :uint32 # default
45
+
46
+ if options[:limit]
47
+ if unsigned
48
+ kind = :uint8 if options[:limit] == 1
49
+ kind = :uint16 if options[:limit] == 2
50
+ kind = :uint32 if [3,4].include?(options[:limit])
51
+ kind = :uint64 if [5,6,7].include?(options[:limit])
52
+ kind = :big_integer if options[:limit] == 8
53
+ kind = :uint256 if options[:limit] > 8
54
+ else
55
+ kind = :int8 if options[:limit] == 1
56
+ kind = :int16 if options[:limit] == 2
57
+ kind = :int32 if [3,4].include?(options[:limit])
58
+ kind = :int64 if options[:limit] > 5 && options[:limit] <= 8
59
+ kind = :int128 if options[:limit] > 8 && options[:limit] <= 16
60
+ kind = :int256 if options[:limit] > 16
61
+ end
42
62
  end
63
+ args.each { |name| column(name, kind, **options.except(:limit, :unsigned)) }
43
64
  end
44
-
45
65
  end
46
66
  end
47
67
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'clickhouse-activerecord/version'
4
+
3
5
  module ActiveRecord
4
6
  module ConnectionAdapters
5
7
  module Clickhouse
@@ -17,8 +19,15 @@ module ActiveRecord
17
19
  def exec_query(sql, name = nil, binds = [], prepare: false)
18
20
  result = do_execute(sql, name)
19
21
  ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
20
- rescue StandardError => _e
21
- raise ActiveRecord::ActiveRecordError, "Response: #{result}"
22
+ rescue ActiveRecord::ActiveRecordError => e
23
+ raise e
24
+ rescue StandardError => e
25
+ raise ActiveRecord::ActiveRecordError, "Response: #{e.message}"
26
+ end
27
+
28
+ def exec_insert_all(sql, name)
29
+ do_execute(sql, name, format: nil)
30
+ true
22
31
  end
23
32
 
24
33
  def exec_update(_sql, _name = nil, _binds = [])
@@ -36,8 +45,8 @@ module ActiveRecord
36
45
  end
37
46
 
38
47
  def table_options(table)
39
- sql = do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first)
40
- { options: sql.gsub(/^(?:.*?)ENGINE = (.*?)$/, '\\1') }
48
+ sql = show_create_table(table)
49
+ { options: sql.gsub(/^(?:.*?)(?:ENGINE = (.*?))?( AS SELECT .*?)?$/, '\\1').presence, as: sql.match(/^CREATE (?:.*?) AS (SELECT .*?)$/).try(:[], 1) }.compact
41
50
  end
42
51
 
43
52
  # Not indexes on clickhouse
@@ -51,7 +60,7 @@ module ActiveRecord
51
60
 
52
61
  def do_system_execute(sql, name = nil)
53
62
  log_with_debug(sql, "#{adapter_name} #{name}") do
54
- res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
63
+ res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact", 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
55
64
 
56
65
  process_response(res)
57
66
  end
@@ -61,7 +70,7 @@ module ActiveRecord
61
70
  log(sql, "#{adapter_name} #{name}") do
62
71
  formatted_sql = apply_format(sql, format)
63
72
  request_params = @config || {}
64
- res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql)
73
+ res = @connection.post("/?#{request_params.merge(settings).to_param}", formatted_sql, 'User-Agent' => "Clickhouse ActiveRecord #{ClickhouseActiverecord::VERSION}")
65
74
 
66
75
  process_response(res)
67
76
  end
@@ -114,8 +123,8 @@ module ActiveRecord
114
123
  Clickhouse::SchemaCreation.new(self)
115
124
  end
116
125
 
117
- def create_table_definition(*args)
118
- Clickhouse::TableDefinition.new(self, *args)
126
+ def create_table_definition(table_name, **options)
127
+ Clickhouse::TableDefinition.new(self, table_name, **options)
119
128
  end
120
129
 
121
130
  def new_column_from_field(table_name, field)
@@ -3,6 +3,7 @@
3
3
  require 'clickhouse-activerecord/arel/visitors/to_sql'
4
4
  require 'clickhouse-activerecord/arel/table'
5
5
  require 'clickhouse-activerecord/migration'
6
+ require 'active_record/connection_adapters/clickhouse/oid/array'
6
7
  require 'active_record/connection_adapters/clickhouse/oid/date'
7
8
  require 'active_record/connection_adapters/clickhouse/oid/date_time'
8
9
  require 'active_record/connection_adapters/clickhouse/oid/big_integer'
@@ -17,9 +18,22 @@ module ActiveRecord
17
18
  # Establishes a connection to the database that's used by all Active Record objects
18
19
  def clickhouse_connection(config)
19
20
  config = config.symbolize_keys
20
- host = config[:host] || 'localhost'
21
- port = config[:port] || 8123
22
- ssl = config[:ssl].present? ? config[:ssl] : port == 443
21
+
22
+ if config[:connection]
23
+ connection = {
24
+ connection: config[:connection]
25
+ }
26
+ else
27
+ port = config[:port] || 8123
28
+ connection = {
29
+ host: config[:host] || 'localhost',
30
+ port: port,
31
+ ssl: config[:ssl].present? ? config[:ssl] : port == 443,
32
+ sslca: config[:sslca],
33
+ read_timeout: config[:read_timeout],
34
+ write_timeout: config[:write_timeout],
35
+ }
36
+ end
23
37
 
24
38
  if config.key?(:database)
25
39
  database = config[:database]
@@ -27,7 +41,7 @@ module ActiveRecord
27
41
  raise ArgumentError, 'No database specified. Missing argument: database.'
28
42
  end
29
43
 
30
- ConnectionAdapters::ClickhouseAdapter.new(logger, [host, port, ssl], { user: config[:username], password: config[:password], database: database }.compact, config)
44
+ ConnectionAdapters::ClickhouseAdapter.new(logger, connection, { user: config[:username], password: config[:password], database: database }.compact, config)
31
45
  end
32
46
  end
33
47
  end
@@ -50,7 +64,11 @@ module ActiveRecord
50
64
  module TypeCaster
51
65
  class Map
52
66
  def is_view
53
- types.is_view
67
+ if @klass.respond_to?(:is_view)
68
+ @klass.is_view # rails 6.1
69
+ else
70
+ types.is_view # less than 6.1
71
+ end
54
72
  end
55
73
  end
56
74
  end
@@ -79,7 +97,6 @@ module ActiveRecord
79
97
 
80
98
  class ClickhouseAdapter < AbstractAdapter
81
99
  ADAPTER_NAME = 'Clickhouse'.freeze
82
-
83
100
  NATIVE_DATABASE_TYPES = {
84
101
  string: { name: 'String' },
85
102
  integer: { name: 'UInt32' },
@@ -88,7 +105,21 @@ module ActiveRecord
88
105
  decimal: { name: 'Decimal' },
89
106
  datetime: { name: 'DateTime' },
90
107
  date: { name: 'Date' },
91
- boolean: { name: 'UInt8' }
108
+ boolean: { name: 'UInt8' },
109
+
110
+ int8: { name: 'Int8' },
111
+ int16: { name: 'Int16' },
112
+ int32: { name: 'Int32' },
113
+ int64: { name: 'Int64' },
114
+ int128: { name: 'Int128' },
115
+ int256: { name: 'Int256' },
116
+
117
+ uint8: { name: 'UInt8' },
118
+ uint16: { name: 'UInt16' },
119
+ uint32: { name: 'UInt32' },
120
+ uint64: { name: 'UInt64' },
121
+ # uint128: { name: 'UInt128' }, not yet implemented in clickhouse
122
+ uint256: { name: 'UInt256' },
92
123
  }.freeze
93
124
 
94
125
  include Clickhouse::SchemaStatements
@@ -139,10 +170,12 @@ module ActiveRecord
139
170
  when /(Nullable)?\(?String\)?/
140
171
  super('String')
141
172
  when /(Nullable)?\(?U?Int8\)?/
142
- super('int2')
143
- when /(Nullable)?\(?U?Int(16|32)\)?/
144
- super('int4')
145
- when /(Nullable)?\(?U?Int(64)\)?/
173
+ 1
174
+ when /(Nullable)?\(?U?Int16\)?/
175
+ 2
176
+ when /(Nullable)?\(?U?Int32\)?/
177
+ nil
178
+ when /(Nullable)?\(?U?Int64\)?/
146
179
  8
147
180
  else
148
181
  super
@@ -154,14 +187,24 @@ module ActiveRecord
154
187
  register_class_with_limit m, %r(String), Type::String
155
188
  register_class_with_limit m, 'Date', Clickhouse::OID::Date
156
189
  register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
157
- register_class_with_limit m, %r(Uint8), Type::UnsignedInteger
158
- m.alias_type 'UInt16', 'UInt8'
159
- m.alias_type 'UInt32', 'UInt8'
160
- register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
190
+
161
191
  register_class_with_limit m, %r(Int8), Type::Integer
162
- m.alias_type 'Int16', 'Int8'
163
- m.alias_type 'Int32', 'Int8'
192
+ register_class_with_limit m, %r(Int16), Type::Integer
193
+ register_class_with_limit m, %r(Int32), Type::Integer
164
194
  register_class_with_limit m, %r(Int64), Type::Integer
195
+ register_class_with_limit m, %r(Int128), Type::Integer
196
+ register_class_with_limit m, %r(Int256), Type::Integer
197
+
198
+ register_class_with_limit m, %r(UInt8), Type::UnsignedInteger
199
+ register_class_with_limit m, %r(UInt16), Type::UnsignedInteger
200
+ register_class_with_limit m, %r(UInt32), Type::UnsignedInteger
201
+ register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
202
+ #register_class_with_limit m, %r(UInt128), Type::UnsignedInteger #not implemnted in clickhouse
203
+ register_class_with_limit m, %r(UInt256), Type::UnsignedInteger
204
+ # register_class_with_limit m, %r(Array), Clickhouse::OID::Array
205
+ m.register_type(%r(Array)) do |sql_type|
206
+ Clickhouse::OID::Array.new(sql_type)
207
+ end
165
208
  end
166
209
 
167
210
  # Quoting time without microseconds
@@ -201,6 +244,12 @@ module ActiveRecord
201
244
  ClickhouseActiverecord::SchemaDumper.create(self, options)
202
245
  end
203
246
 
247
+ # @param [String] table
248
+ # @return [String]
249
+ def show_create_table(table)
250
+ do_system_execute("SHOW CREATE TABLE `#{table}`")['data'].try(:first).try(:first).gsub(/[\n\s]+/m, ' ')
251
+ end
252
+
204
253
  # Create a new ClickHouse database.
205
254
  def create_database(name)
206
255
  sql = apply_cluster "CREATE DATABASE #{quote_table_name(name)}"
@@ -213,7 +262,7 @@ module ActiveRecord
213
262
  def create_view(table_name, **options)
214
263
  options.merge!(view: true)
215
264
  options = apply_replica(table_name, options)
216
- td = create_table_definition(apply_cluster(table_name), options)
265
+ td = create_table_definition(apply_cluster(table_name), **options)
217
266
  yield td if block_given?
218
267
 
219
268
  if options[:force]
@@ -223,10 +272,10 @@ module ActiveRecord
223
272
  execute schema_creation.accept td
224
273
  end
225
274
 
226
- def create_table(table_name, **options)
275
+ def create_table(table_name, **options, &block)
227
276
  options = apply_replica(table_name, options)
228
- td = create_table_definition(apply_cluster(table_name), options)
229
- yield td if block_given?
277
+ td = create_table_definition(apply_cluster(table_name), **options)
278
+ block.call td if block_given?
230
279
 
231
280
  if options[:force]
232
281
  drop_table(table_name, options.merge(if_exists: true))
@@ -235,6 +284,19 @@ module ActiveRecord
235
284
  execute schema_creation.accept td
236
285
  end
237
286
 
287
+ def create_table_with_distributed(table_name, **options, &block)
288
+ sharding_key = options.delete(:sharding_key) || 'rand()'
289
+ create_table("#{table_name}_distributed", **options, &block)
290
+ raise 'Set a cluster' unless cluster
291
+
292
+ distributed_options = "Distributed(#{cluster},#{@config[:database]},#{table_name}_distributed,#{sharding_key})"
293
+ create_table(table_name, **options.merge(options: distributed_options), &block)
294
+ end
295
+
296
+ def drop_table_with_distributed(table_name, **options)
297
+ ["#{table_name}_distributed", table_name].each { |name| drop_table(name, **options) }
298
+ end
299
+
238
300
  # Drops a ClickHouse database.
239
301
  def drop_database(name) #:nodoc:
240
302
  sql = apply_cluster "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
@@ -275,14 +337,41 @@ module ActiveRecord
275
337
  @full_config[:replica_name]
276
338
  end
277
339
 
340
+ def use_default_replicated_merge_tree_params?
341
+ database_engine_atomic? && @full_config[:use_default_replicated_merge_tree_params]
342
+ end
343
+
344
+ def use_replica?
345
+ (replica || use_default_replicated_merge_tree_params?) && cluster
346
+ end
347
+
278
348
  def replica_path(table)
279
349
  "/clickhouse/tables/#{cluster}/#{@config[:database]}.#{table}"
280
350
  end
281
351
 
352
+ def database_engine_atomic?
353
+ current_database_engine = "select engine from system.databases where name = '#{@config[:database]}'"
354
+ res = ActiveRecord::Base.connection.select_one(current_database_engine)
355
+ res['engine'] == 'Atomic' if res
356
+ end
357
+
282
358
  def apply_cluster(sql)
283
359
  cluster ? "#{sql} ON CLUSTER #{cluster}" : sql
284
360
  end
285
361
 
362
+ def supports_insert_on_duplicate_skip?
363
+ true
364
+ end
365
+
366
+ def supports_insert_on_duplicate_update?
367
+ true
368
+ end
369
+
370
+ def build_insert_sql(insert) # :nodoc:
371
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
372
+ sql
373
+ end
374
+
286
375
  protected
287
376
 
288
377
  def last_inserted_id(result)
@@ -298,18 +387,36 @@ module ActiveRecord
298
387
  private
299
388
 
300
389
  def connect
301
- @connection = Net::HTTP.start(@connection_parameters[0], @connection_parameters[1], use_ssl: @connection_parameters[2], verify_mode: OpenSSL::SSL::VERIFY_NONE)
390
+ @connection = @connection_parameters[:connection] || Net::HTTP.start(@connection_parameters[:host], @connection_parameters[:port], use_ssl: @connection_parameters[:ssl], verify_mode: OpenSSL::SSL::VERIFY_NONE)
391
+
392
+ @connection.ca_file = @connection_parameters[:ca_file] if @connection_parameters[:ca_file]
393
+ @connection.read_timeout = @connection_parameters[:read_timeout] if @connection_parameters[:read_timeout]
394
+ @connection.write_timeout = @connection_parameters[:write_timeout] if @connection_parameters[:write_timeout]
395
+
396
+ @connection
302
397
  end
303
398
 
304
399
  def apply_replica(table, options)
305
- if replica && cluster && options[:options]
306
- match = options[:options].match(/^(.*?MergeTree)\(([^\)]*)\)(.*?)$/)
307
- if match
308
- options[:options] = "Replicated#{match[1]}(#{([replica_path(table), replica].map{|v| "'#{v}'"} + [match[2].presence]).compact.join(', ')})#{match[3]}"
400
+ if use_replica? && options[:options]
401
+ if options[:options].match(/^Replicated/)
402
+ raise 'Do not try create Replicated table. It will be configured based on the *MergeTree engine.'
309
403
  end
404
+
405
+ options[:options] = configure_replica(table, options[:options])
310
406
  end
311
407
  options
312
408
  end
409
+
410
+ def configure_replica(table, options)
411
+ match = options.match(/^(.*?MergeTree)(?:\(([^\)]*)\))?((?:.|\n)*)/)
412
+ return options unless match
413
+
414
+ if replica
415
+ engine_params = ([replica_path(table), replica].map { |v| "'#{v}'" } + [match[2].presence]).compact.join(', ')
416
+ end
417
+
418
+ "Replicated#{match[1]}(#{engine_params})#{match[3]}"
419
+ end
313
420
  end
314
421
  end
315
422
  end
@@ -1,17 +1,28 @@
1
+ require 'active_record/migration'
2
+
1
3
  module ClickhouseActiverecord
2
4
 
3
5
  class SchemaMigration < ::ActiveRecord::SchemaMigration
4
6
  class << self
5
7
 
6
8
  def create_table
7
- unless table_exists?
8
- version_options = connection.internal_string_options_for_primary_key
9
-
10
- connection.create_table(table_name, id: false, options: 'ReplacingMergeTree(ver) PARTITION BY version ORDER BY (version)', if_not_exists: true) do |t|
11
- t.string :version, version_options
12
- t.column :active, 'Int8', null: false, default: '1'
13
- t.datetime :ver, null: false, default: -> { 'now()' }
14
- end
9
+ return if table_exists?
10
+
11
+ version_options = connection.internal_string_options_for_primary_key
12
+ table_options = {
13
+ id: false, options: 'ReplacingMergeTree(ver) PARTITION BY version ORDER BY (version)', if_not_exists: true
14
+ }
15
+ if connection.instance_variable_get(:@full_config)[:distributed_service_tables]
16
+ table_options.merge!(sharding_key: 'cityHash64(version)')
17
+ table_creation_method = 'create_table_with_distributed'
18
+ else
19
+ table_creation_method = 'create_table'
20
+ end
21
+
22
+ connection.public_send(table_creation_method, table_name, **table_options) do |t|
23
+ t.string :version, **version_options
24
+ t.column :active, 'Int8', null: false, default: '1'
25
+ t.datetime :ver, null: false, default: -> { 'now()' }
15
26
  end
16
27
  end
17
28
 
@@ -24,14 +35,25 @@ module ClickhouseActiverecord
24
35
  class InternalMetadata < ::ActiveRecord::InternalMetadata
25
36
  class << self
26
37
  def create_table
27
- unless table_exists?
28
- key_options = connection.internal_string_options_for_primary_key
29
-
30
- connection.create_table(table_name, id: false, options: 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)', if_not_exists: true) do |t|
31
- t.string :key, key_options
32
- t.string :value
33
- t.timestamps
34
- end
38
+ return if table_exists?
39
+
40
+ key_options = connection.internal_string_options_for_primary_key
41
+ table_options = {
42
+ id: false,
43
+ options: connection.adapter_name.downcase == 'clickhouse' ? 'MergeTree() PARTITION BY toDate(created_at) ORDER BY (created_at)' : '',
44
+ if_not_exists: true
45
+ }
46
+ if connection.instance_variable_get(:@full_config).try(:[], :distributed_service_tables)
47
+ table_options.merge!(sharding_key: 'cityHash64(created_at)')
48
+ table_creation_method = 'create_table_with_distributed'
49
+ else
50
+ table_creation_method = 'create_table'
51
+ end
52
+
53
+ connection.public_send(table_creation_method, table_name, **table_options) do |t|
54
+ t.string :key, **key_options
55
+ t.string :value
56
+ t.timestamps
35
57
  end
36
58
  end
37
59
  end
@@ -45,6 +67,16 @@ module ClickhouseActiverecord
45
67
  @schema_migration = schema_migration
46
68
  end
47
69
 
70
+ def up(target_version = nil)
71
+ selected_migrations = if block_given?
72
+ migrations.select { |m| yield m }
73
+ else
74
+ migrations
75
+ end
76
+
77
+ ClickhouseActiverecord::Migrator.new(:up, selected_migrations, schema_migration, target_version).migrate
78
+ end
79
+
48
80
  def down(target_version = nil)
49
81
  selected_migrations = if block_given?
50
82
  migrations.select { |m| yield m }
@@ -33,16 +33,24 @@ module ClickhouseActiverecord
33
33
  HEADER
34
34
  end
35
35
 
36
+ def tables(stream)
37
+ sorted_tables = @connection.tables.sort {|a,b| @connection.show_create_table(a).match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/) ? 1 : a <=> b }
38
+
39
+ sorted_tables.each do |table_name|
40
+ table(table_name, stream) unless ignored?(table_name)
41
+ end
42
+ end
43
+
36
44
  def table(table, stream)
37
- if table.match(/^\.inner\./).nil?
45
+ if table.match(/^\.inner/).nil?
38
46
  unless simple
39
47
  stream.puts " # TABLE: #{table}"
40
- sql = @connection.do_system_execute("SHOW CREATE TABLE `#{table.gsub(/^\.inner\./, '')}`")['data'].try(:first).try(:first)
48
+ sql = @connection.show_create_table(table)
41
49
  stream.puts " # SQL: #{sql.gsub(/ENGINE = Replicated(.*?)\('[^']+',\s*'[^']+',?\s?([^\)]*)?\)/, "ENGINE = \\1(\\2)")}" if sql
42
50
  # super(table.gsub(/^\.inner\./, ''), stream)
43
51
 
44
52
  # detect view table
45
- match = sql.match(/^CREATE\s+(MATERIALIZED)\s+VIEW/)
53
+ match = sql.match(/^CREATE\s+(MATERIALIZED\s+)?VIEW/)
46
54
  end
47
55
 
48
56
  # Copy from original dumper
@@ -125,5 +133,26 @@ HEADER
125
133
  super
126
134
  end
127
135
  end
136
+
137
+ def schema_limit(column)
138
+ return nil if column.type == :float
139
+ super
140
+ end
141
+
142
+ def schema_unsigned(column)
143
+ return nil unless column.type == :integer && !simple
144
+ (column.sql_type =~ /(Nullable)?\(?UInt\d+\)?/).nil? ? false : nil
145
+ end
146
+
147
+ def schema_array(column)
148
+ (column.sql_type =~ /Array?\(/).nil? ? nil : true
149
+ end
150
+
151
+ def prepare_column_options(column)
152
+ spec = {}
153
+ spec[:unsigned] = schema_unsigned(column)
154
+ spec[:array] = schema_array(column)
155
+ spec.merge(super).compact
156
+ end
128
157
  end
129
158
  end
@@ -6,7 +6,7 @@ module ClickhouseActiverecord
6
6
  delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base
7
7
 
8
8
  def initialize(configuration)
9
- @configuration = configuration
9
+ @configuration = configuration.with_indifferent_access
10
10
  end
11
11
 
12
12
  def create
@@ -43,7 +43,15 @@ module ClickhouseActiverecord
43
43
  end
44
44
 
45
45
  def structure_load(*args)
46
- File.read(args.first).split(";\n\n").each { |sql| connection.execute(sql) }
46
+ File.read(args.first).split(";\n\n").each do |sql|
47
+ if sql.gsub(/[a-z]/i, '').blank?
48
+ next
49
+ elsif sql =~ /^INSERT INTO/
50
+ connection.do_execute(sql, nil, format: nil)
51
+ else
52
+ connection.execute(sql)
53
+ end
54
+ end
47
55
  end
48
56
 
49
57
  def migrate
@@ -1,3 +1,3 @@
1
1
  module ClickhouseActiverecord
2
- VERSION = '0.4.7'
2
+ VERSION = '0.5.7'
3
3
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'active_record/connection_adapters/clickhouse_adapter'
4
4
 
5
+ require_relative '../core_extensions/active_record/migration/command_recorder'
6
+ ActiveRecord::Migration::CommandRecorder.include CoreExtensions::ActiveRecord::Migration::CommandRecorder
7
+
5
8
  if defined?(Rails::Railtie)
6
9
  require 'clickhouse-activerecord/railtie'
7
10
  require 'clickhouse-activerecord/schema'
@@ -13,7 +13,7 @@ class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerato
13
13
 
14
14
  def db_migrate_path
15
15
  if defined?(Rails.application) && Rails.application && respond_to?(:configured_migrate_path, true)
16
- configured_migrate_path
16
+ configured_migrate_path || default_migrate_path
17
17
  else
18
18
  default_migrate_path
19
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: 0.4.7
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-25 00:00:00.000000000 Z
11
+ date: 2021-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -112,6 +112,8 @@ files:
112
112
  - bin/console
113
113
  - bin/setup
114
114
  - clickhouse-activerecord.gemspec
115
+ - core_extensions/active_record/migration/command_recorder.rb
116
+ - lib/active_record/connection_adapters/clickhouse/oid/array.rb
115
117
  - lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
116
118
  - lib/active_record/connection_adapters/clickhouse/oid/date.rb
117
119
  - lib/active_record/connection_adapters/clickhouse/oid/date_time.rb
@@ -134,7 +136,7 @@ homepage: https://github.com/pnixx/clickhouse-activerecord
134
136
  licenses:
135
137
  - MIT
136
138
  metadata: {}
137
- post_install_message:
139
+ post_install_message:
138
140
  rdoc_options: []
139
141
  require_paths:
140
142
  - lib
@@ -149,8 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  - !ruby/object:Gem::Version
150
152
  version: '0'
151
153
  requirements: []
152
- rubygems_version: 3.0.1
153
- signing_key:
154
+ rubygems_version: 3.0.3
155
+ signing_key:
154
156
  specification_version: 4
155
157
  summary: ClickHouse ActiveRecord
156
158
  test_files: []