clickhouse-activerecord 0.4.7 → 0.5.7

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: 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: []