clickhouse-activerecord 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a62437ebce5fafd239daf41a6d83aea137fbd02a
4
- data.tar.gz: 1af618f104ee85ded1fc71a5dd28893ef34f5925
2
+ SHA256:
3
+ metadata.gz: 8a3c9c8eca31fb8287bb89238201bb073223e84af6828fe5331aa4b18b9ed7b2
4
+ data.tar.gz: 379946b27adc6065995dbafd4fbd65cdbf4a9bebe5e89b2dcb460136b229382a
5
5
  SHA512:
6
- metadata.gz: b94de9c7bc6c2d9a09841dc14ae95a9602781818f7ea21c2a65d99213c7797d45437fcf6ef82d8e3a8fc9626236d3cad32593fc0f06cffe7be21d7571984ceca
7
- data.tar.gz: a1afba5dacfde680fce68a398447c1470ab852892e77fc54c277ea372203168723ca5789d6b7b0d1ab5a11c441459a3ed6e26ec2865bb36e3d14002ab69baacb
6
+ metadata.gz: 2f8048d478a31674f135a9aebff80a588c31fcfd153b3264fcd28d0c2d36d3dc5785d96056954caa3ab037d33e5e24ff97fd61337058b875baa5f14922abae2d
7
+ data.tar.gz: 81a6bf828d38da1dfd71fc58bf7408e84407aa1c6156c64412e28247815536b231d79dc84481c417ca75eff563d5e8ebf2d149ac4eba40a717fef9d35abb1450
data/.gitignore CHANGED
@@ -53,3 +53,4 @@ com_crashlytics_export_strings.xml
53
53
  crashlytics.properties
54
54
  crashlytics-build.properties
55
55
  fabric.properties
56
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ### Version 0.3.0 (Nov 27, 2018)
2
+
3
+ * Support materialized view
4
+ * Aggregated functions for view
5
+ * Schema dumper with SQL create table
6
+ * Added migrations support [@Bugagazavr](https://github.com/Bugagazavr)
7
+
8
+ ### Version 0.2.0 (Oct 3, 2017)
9
+
10
+ * Support Rails 5.0
11
+
12
+ ### Version 0.1.2 (Sep 27, 2017)
13
+
14
+ * Fix Big Int type
15
+
16
+ ### Version 0.1.0 (Aug 31, 2017)
17
+
18
+ * Initial release
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Clickhouse::Activerecord
2
2
 
3
- A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.0.
3
+ A Ruby database ActiveRecord driver for ClickHouse. Support Rails >= 5.2.
4
4
 
5
5
  ## Installation
6
6
 
@@ -29,6 +29,7 @@ development_clickhouse:
29
29
  host: localhost
30
30
  username: username
31
31
  password: password
32
+ debug: true # use for showing in to log technical information
32
33
  ```
33
34
 
34
35
  Add to your model:
@@ -39,7 +40,15 @@ class Action < ActiveRecord::Base
39
40
  end
40
41
  ```
41
42
 
42
- Or global connection, but schema dump don't works:
43
+ For materialized view model add:
44
+ ```ruby
45
+ class ActionView < ActiveRecord::Base
46
+ establish_connection "#{Rails.env}_clickhouse".to_sym
47
+ self.is_view = true
48
+ end
49
+ ```
50
+
51
+ Or global connection:
43
52
 
44
53
  ```yml
45
54
  development:
@@ -50,21 +59,55 @@ development:
50
59
  password: password
51
60
  ```
52
61
 
53
- Schema dump:
62
+ ### Rake tasks
63
+
64
+ Create / drop / purge / reset database:
65
+
66
+ $ rake clickhouse:create
67
+ $ rake clickhouse:drop
68
+ $ rake clickhouse:purge
69
+ $ rake clickhouse:reset
70
+
71
+ Migration:
72
+
73
+ $ rails g clickhouse_migration MIGRATION_NAME COLUMNS
74
+ $ rake clickhouse:migrate
75
+
76
+ Rollback migration not supported!
77
+
78
+ Schema dump to `db/clickhouse_schema.rb` file:
54
79
 
55
80
  $ rake clickhouse:schema:dump
56
81
 
82
+ Schema load from `db/clickhouse_schema.rb` file:
83
+
84
+ $ rake clickhouse:schema:load
85
+
86
+ We use schema for emulate development or tests environment on PostgreSQL adapter.
87
+
57
88
  ### Insert and select data
58
89
 
59
90
  ```ruby
60
91
  Action.where(url: 'http://example.com', date: Date.current).where.not(name: nil).order(created_at: :desc).limit(10)
61
- => #<ActiveRecord::Relation [#<Action *** >]>
92
+ # Clickhouse Action Load (10.3ms) SELECT actions.* FROM actions WHERE actions.date = '2017-11-29' AND actions.url = 'http://example.com' AND (actions.name IS NOT NULL) ORDER BY actions.created_at DESC LIMIT 10
93
+ #=> #<ActiveRecord::Relation [#<Action *** >]>
62
94
 
63
95
  Action.create(url: 'http://example.com', date: Date.yesterday)
64
- => true
96
+ # Clickhouse Action Load (10.8ms) INSERT INTO actions (url, date) VALUES ('http://example.com', '2017-11-28')
97
+ #=> true
98
+
99
+ ActionView.maximum(:date)
100
+ # Clickhouse (10.3ms) SELECT maxMerge(actions.date) FROM actions
101
+ #=> 'Wed, 29 Nov 2017'
65
102
  ```
66
103
 
67
- NOTE: Creating tables in developing.
104
+ ## Donations
105
+
106
+ Donations to this project are going directly to [PNixx](https://github.com/PNixx), the original author of this project:
107
+
108
+ * BTC address: `1Lx2gaJtzfF2dxGFxB65YtY5kNY9xUi6ia`
109
+ * ETH address: `0x6F094365A70fe7836A633d2eE80A1FA9758234d5`
110
+ * XMR address: `42gP71qLB5M43RuDnrQ3vSJFFxis9Kw9VMURhpx9NLQRRwNvaZRjm2TFojAMC8Fk1BQhZNKyWhoyJSn5Ak9kppgZPjE17Zh`
68
111
 
69
112
  ## Development
70
113
 
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
1
2
  # coding: utf-8
2
3
 
3
4
  lib = File.expand_path('../lib', __FILE__)
4
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require File.expand_path('../lib/clickhouse-activerecord/version', __FILE__)
5
7
 
6
8
  Gem::Specification.new do |spec|
7
9
  spec.name = 'clickhouse-activerecord'
8
- spec.version = '0.2.2'
10
+ spec.version = ClickhouseActiverecord::VERSION
9
11
  spec.authors = ['Sergey Odintsov']
10
12
  spec.email = ['nixx.dj@gmail.com']
11
13
 
@@ -22,8 +24,10 @@ Gem::Specification.new do |spec|
22
24
  spec.require_paths = ['lib']
23
25
 
24
26
  spec.add_dependency "bundler", ">= 1.13.4"
25
- spec.add_dependency 'rails', '>= 5.0'
27
+ spec.add_dependency 'activerecord', '>= 5.2'
26
28
 
27
29
  spec.add_development_dependency 'bundler', '~> 1.15'
28
30
  spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec'
32
+ spec.add_development_dependency 'pry'
29
33
  end
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module Clickhouse
4
6
  module OID # :nodoc:
5
7
  class BigInteger < Type::BigInteger # :nodoc:
6
-
7
- DEFAULT_LIMIT = 8
8
+ def type
9
+ :big_integer
10
+ end
8
11
 
9
12
  def limit
10
13
  DEFAULT_LIMIT
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module Clickhouse
@@ -1,9 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module Clickhouse
4
6
  module OID # :nodoc:
5
7
  class DateTime < Type::DateTime # :nodoc:
6
8
 
9
+ def serialize(value)
10
+ value = super
11
+ return value unless value.acts_like?(:time)
12
+
13
+ value.to_time.strftime('%Y-%m-%d %H:%M:%S')
14
+ end
15
+
7
16
  def type_cast_from_database(value)
8
17
  value
9
18
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Clickhouse
6
+ class SchemaCreation < AbstractAdapter::SchemaCreation# :nodoc:
7
+
8
+ def visit_AddColumnDefinition(o)
9
+ +"ADD COLUMN #{accept(o.column)}"
10
+ end
11
+
12
+ def add_column_options!(sql, options)
13
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
14
+ if options[:null] || options[:null].nil?
15
+ sql.gsub!(/\s+(.*)/, ' Nullable(\1)')
16
+ end
17
+ sql.gsub!(/(\sString)\(\d+\)/, '\1')
18
+ sql
19
+ end
20
+
21
+ def add_table_options!(create_sql, options)
22
+ if options[:options].present?
23
+ create_sql << " ENGINE = #{options[:options]}"
24
+ else
25
+ create_sql << " ENGINE = Log()"
26
+ end
27
+
28
+ create_sql
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Clickhouse
6
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
7
+
8
+ def integer(*args, **options)
9
+ if options[:limit] == 8
10
+ args.each { |name| column(name, :big_integer, options.except(:limit)) }
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Clickhouse
6
+ module SchemaStatements
7
+ def execute(sql, name = nil)
8
+ do_execute(sql, name)
9
+ end
10
+
11
+ def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil)
12
+ new_sql = sql.dup.sub(/ (DEFAULT )?VALUES/, " VALUES")
13
+ do_execute(new_sql, name, format: nil)
14
+ true
15
+ end
16
+
17
+ def exec_query(sql, name = nil, binds = [], prepare: false)
18
+ result = do_execute(sql, name)
19
+ ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
20
+ end
21
+
22
+ def exec_update(_sql, _name = nil, _binds = [])
23
+ raise ActiveRecord::ActiveRecordError, 'Clickhouse update is not supported'
24
+ end
25
+
26
+ def exec_delete(_sql, _name = nil, _binds = [])
27
+ raise ActiveRecord::ActiveRecordError, 'Clickhouse delete is not supported'
28
+ end
29
+
30
+ def tables(name = nil)
31
+ result = do_system_execute('SHOW TABLES', name)
32
+ return [] if result.nil?
33
+ result['data'].flatten
34
+ end
35
+
36
+ def table_options(table)
37
+ sql = do_system_execute("SHOW CREATE TABLE #{table}")['data'].try(:first).try(:first)
38
+ { options: sql.gsub(/^(?:.*?)ENGINE = (.*?)$/, '\\1') }
39
+ end
40
+
41
+ # @todo copied from ActiveRecord::ConnectionAdapters::SchemaStatements v5.2.2
42
+ # Why version column type of String, but insert to Integer?
43
+ def assume_migrated_upto_version(version, migrations_paths)
44
+ migrations_paths = Array(migrations_paths)
45
+ version = version.to_i
46
+ sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
47
+
48
+ migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i)
49
+ versions = migration_context.migration_files.map do |file|
50
+ migration_context.parse_migration_filename(file).first.to_i
51
+ end
52
+
53
+ unless migrated.include?(version)
54
+ do_execute( "INSERT INTO #{sm_table} (version) VALUES (#{quote(version.to_s)})", 'SchemaMigration', format: nil)
55
+ end
56
+
57
+ inserting = (versions - migrated).select { |v| v < version }
58
+ if inserting.any?
59
+ if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
60
+ raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
61
+ end
62
+ if supports_multi_insert?
63
+ do_system_execute insert_versions_sql(inserting)
64
+ else
65
+ inserting.each do |v|
66
+ do_system_execute insert_versions_sql(v)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # Not indexes on clickhouse
73
+ def indexes(table_name, name = nil)
74
+ []
75
+ end
76
+
77
+ def data_sources
78
+ tables
79
+ end
80
+
81
+ def do_system_execute(sql, name = nil)
82
+ log_with_debug(sql, "#{adapter_name} #{name}") do
83
+ res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
84
+
85
+ process_response(res)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def apply_format(sql, format)
92
+ format ? "#{sql} FORMAT #{format}" : sql
93
+ end
94
+
95
+ def do_execute(sql, name = nil, format: 'JSONCompact')
96
+ log(sql, "#{adapter_name} #{name}") do
97
+ formatted_sql = apply_format(sql, format)
98
+ res = @connection.post("/?#{@config.to_param}", formatted_sql)
99
+
100
+ process_response(res)
101
+ end
102
+ end
103
+
104
+ def process_response(res)
105
+ case res.code.to_i
106
+ when 200
107
+ res.body.presence && JSON.parse(res.body)
108
+ else
109
+ raise ActiveRecord::ActiveRecordError,
110
+ "Response code: #{res.code}:\n#{res.body}"
111
+ end
112
+ end
113
+
114
+ def log_with_debug(sql, name = nil)
115
+ return yield unless @debug
116
+ log(sql, "#{name} (system)") { yield }
117
+ end
118
+
119
+ def schema_creation
120
+ Clickhouse::SchemaCreation.new(self)
121
+ end
122
+
123
+ def create_table_definition(*args)
124
+ Clickhouse::TableDefinition.new(*args)
125
+ end
126
+
127
+ def new_column_from_field(table_name, field)
128
+ sql_type = field[1]
129
+ type_metadata = fetch_type_metadata(sql_type)
130
+ default = field[3]
131
+ default_value = extract_value_from_default(default)
132
+ default_function = extract_default_function(default_value, default)
133
+ ClickhouseColumn.new(field[0], default_value, type_metadata, field[1].include?('Nullable'), table_name, default_function)
134
+ end
135
+
136
+ protected
137
+
138
+ def table_structure(table_name)
139
+ result = do_system_execute("DESCRIBE TABLE #{table_name}", table_name)
140
+ data = result['data']
141
+
142
+ return data unless data.empty?
143
+
144
+ raise ActiveRecord::StatementInvalid,
145
+ "Could not find table '#{table_name}'"
146
+ end
147
+ alias column_definitions table_structure
148
+
149
+ private
150
+
151
+ # Extracts the value from a PostgreSQL column default definition.
152
+ def extract_value_from_default(default)
153
+ case default
154
+ # Quoted types
155
+ when /\Anow\(\)\z/m
156
+ nil
157
+ # Boolean types
158
+ when "true".freeze, "false".freeze
159
+ default
160
+ # Object identifier types
161
+ when /\A-?\d+\z/
162
+ $1
163
+ else
164
+ # Anything else is blank, some user type, or some function
165
+ # and we can't know the value of that, so return nil.
166
+ nil
167
+ end
168
+ end
169
+
170
+ def extract_default_function(default_value, default) # :nodoc:
171
+ default if has_default_function?(default_value, default)
172
+ end
173
+
174
+ def has_default_function?(default_value, default) # :nodoc:
175
+ !default_value && (%r{\w+\(.*\)} === default)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clickhouse-activerecord/arel/visitors/to_sql'
1
4
  require 'active_record/connection_adapters/abstract_adapter'
2
5
  require 'active_record/connection_adapters/clickhouse/oid/date'
3
6
  require 'active_record/connection_adapters/clickhouse/oid/date_time'
4
7
  require 'active_record/connection_adapters/clickhouse/oid/big_integer'
8
+ require 'active_record/connection_adapters/clickhouse/schema_definitions'
9
+ require 'active_record/connection_adapters/clickhouse/schema_creation'
10
+ require 'active_record/connection_adapters/clickhouse/schema_statements'
11
+ require 'net/http'
5
12
 
6
13
  module ActiveRecord
7
14
  class Base
@@ -9,7 +16,7 @@ module ActiveRecord
9
16
  # Establishes a connection to the database that's used by all Active Record objects
10
17
  def clickhouse_connection(config)
11
18
  config = config.symbolize_keys
12
- host = config[:host]
19
+ host = config[:host] || 'localhost'
13
20
  port = config[:port] || 8123
14
21
 
15
22
  if config.key?(:database)
@@ -18,51 +25,25 @@ module ActiveRecord
18
25
  raise ArgumentError, 'No database specified. Missing argument: database.'
19
26
  end
20
27
 
21
- ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact)
28
+ ConnectionAdapters::ClickhouseAdapter.new(nil, logger, [host, port], { user: config[:username], password: config[:password], database: database }.compact, config[:debug])
22
29
  end
23
30
  end
24
31
  end
25
32
 
26
- module ConnectionAdapters
27
-
28
- class ClickhouseColumn < Column
29
-
30
- private
31
-
32
- # Extracts the value from a PostgreSQL column default definition.
33
- def extract_value_from_default(default)
34
- case default
35
- # Quoted types
36
- when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
37
- # The default 'now'::date is CURRENT_DATE
38
- if $1 == "now".freeze && $2 == "date".freeze
39
- nil
40
- else
41
- $1.gsub("''".freeze, "'".freeze)
42
- end
43
- # Boolean types
44
- when "true".freeze, "false".freeze
45
- default
46
- # Numeric types
47
- when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
48
- $1
49
- # Object identifier types
50
- when /\A-?\d+\z/
51
- $1
52
- else
53
- # Anything else is blank, some user type, or some function
54
- # and we can't know the value of that, so return nil.
55
- nil
56
- end
33
+ module ModelSchema
34
+ module ClassMethods
35
+ def is_view
36
+ @is_view || false
57
37
  end
58
-
59
- def extract_default_function(default_value, default) # :nodoc:
60
- default if has_default_function?(default_value, default)
38
+ # @param [Boolean] value
39
+ def is_view=(value)
40
+ @is_view = value
61
41
  end
42
+ end
43
+ end
62
44
 
63
- def has_default_function?(default_value, default) # :nodoc:
64
- !default_value && (%r{\w+\(.*\)} === default)
65
- end
45
+ module ConnectionAdapters
46
+ class ClickhouseColumn < Column
66
47
 
67
48
  end
68
49
 
@@ -70,26 +51,34 @@ module ActiveRecord
70
51
  ADAPTER_NAME = 'Clickhouse'.freeze
71
52
 
72
53
  NATIVE_DATABASE_TYPES = {
73
- string: { name: 'String', limit: 255 },
54
+ string: { name: 'String' },
74
55
  integer: { name: 'UInt32' },
75
- big_integer: { name: 'UInt64', limit: 8 },
56
+ big_integer: { name: 'UInt64' },
76
57
  float: { name: 'Float32' },
58
+ decimal: { name: 'Decimal' },
77
59
  datetime: { name: 'DateTime' },
78
60
  date: { name: 'Date' },
79
61
  boolean: { name: 'UInt8' }
80
62
  }.freeze
81
63
 
64
+ include Clickhouse::SchemaStatements
65
+
82
66
  # Initializes and connects a Clickhouse adapter.
83
- def initialize(connection, logger, connection_parameters, config)
67
+ def initialize(connection, logger, connection_parameters, config, debug = false)
84
68
  super(connection, logger)
85
69
  @connection_parameters = connection_parameters
86
70
  @config = config
71
+ @debug = debug
87
72
 
88
73
  @prepared_statements = false
89
74
 
90
75
  connect
91
76
  end
92
77
 
78
+ def arel_visitor # :nodoc:
79
+ ClickhouseActiverecord::Arel::Visitors::ToSql.new(self)
80
+ end
81
+
93
82
  def native_database_types #:nodoc:
94
83
  NATIVE_DATABASE_TYPES
95
84
  end
@@ -100,11 +89,13 @@ module ActiveRecord
100
89
 
101
90
  def extract_limit(sql_type) # :nodoc:
102
91
  case sql_type
103
- when 'Nullable(String)'
104
- 255
105
- when /Nullable\(U?Int(8|16)\)/
106
- 4
107
- when /Nullable\(U?Int(32|64)\)/
92
+ when /(Nullable)?\(?String\)?/
93
+ super('String')
94
+ when /(Nullable)?\(?U?Int8\)?/
95
+ super('int2')
96
+ when /(Nullable)?\(?U?Int(16|32)\)?/
97
+ super('int4')
98
+ when /(Nullable)?\(?U?Int(64)\)?/
108
99
  8
109
100
  else
110
101
  super
@@ -113,93 +104,59 @@ module ActiveRecord
113
104
 
114
105
  def initialize_type_map(m) # :nodoc:
115
106
  super
116
- register_class_with_limit m, 'String', Type::String
117
- register_class_with_limit m, 'Nullable(String)', Type::String
118
- register_class_with_limit m, 'Uint8', Type::UnsignedInteger
107
+ register_class_with_limit m, %r(String), Type::String
119
108
  register_class_with_limit m, 'Date', Clickhouse::OID::Date
120
109
  register_class_with_limit m, 'DateTime', Clickhouse::OID::DateTime
121
- m.alias_type 'UInt16', 'uint4'
122
- m.alias_type 'UInt32', 'uint8'
123
- m.register_type 'UInt64', Clickhouse::OID::BigInteger.new
124
- m.alias_type 'Int8', 'int4'
125
- m.alias_type 'Int16', 'int4'
126
- m.alias_type 'Int32', 'int8'
127
- m.alias_type 'Int64', 'UInt64'
128
- end
129
-
130
- # Queries the database and returns the results in an Array-like object
131
- def query(sql, _name = nil)
132
- res = @connection.post("/?#{@config.to_param}", "#{sql} FORMAT JSONCompact")
133
- raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}" unless res.code.to_i == 200
134
- JSON.parse res.body
135
- end
136
-
137
- def execute(sql, name = nil)
138
- log(sql, "#{adapter_name} #{name}") do
139
- query sql, name
140
- end
141
- end
142
-
143
- def exec_query(sql, name = nil, binds = [], prepare: false)
144
- result = execute(sql, name)
145
- ActiveRecord::Result.new(result['meta'].map { |m| m['name'] }, result['data'])
110
+ register_class_with_limit m, %r(Uint8), Type::UnsignedInteger
111
+ m.alias_type 'UInt16', 'UInt8'
112
+ m.alias_type 'UInt32', 'UInt8'
113
+ register_class_with_limit m, %r(UInt64), Type::UnsignedInteger
114
+ register_class_with_limit m, %r(Int8), Type::Integer
115
+ m.alias_type 'Int16', 'Int8'
116
+ m.alias_type 'Int32', 'Int8'
117
+ register_class_with_limit m, %r(Int64), Type::Integer
146
118
  end
147
119
 
148
120
  # Executes insert +sql+ statement in the context of this connection using
149
121
  # +binds+ as the bind substitutes. +name+ is logged along with
150
122
  # the executed +sql+ statement.
151
- def exec_insert(sql, name, _binds, _pk = nil, _sequence_name = nil)
152
- log(sql, "#{adapter_name} #{name}") do
153
- res = @connection.post("/?#{@config.to_param}", sql)
154
- raise ActiveRecord::ActiveRecordError, "Response code: #{res.code}:\n#{res.body}" unless res.code.to_i == 200
155
- true
156
- end
157
- end
158
-
159
- def update(_arel, _name = nil, _binds = [])
160
- raise ActiveRecord::ActiveRecordError, 'Clickhouse update is not supported'
161
- end
162
-
163
- def delete(_arel, _name = nil, _binds = [])
164
- raise ActiveRecord::ActiveRecordError, 'Clickhouse delete is not supported'
165
- end
166
123
 
167
124
  # SCHEMA STATEMENTS ========================================
168
125
 
169
- def tables(name = nil)
170
- query('SHOW TABLES', name)['data'].flatten
126
+ def primary_key(table_name) #:nodoc:
127
+ pk = table_structure(table_name).first
128
+ return 'id' if pk.present? && pk[0] == 'id'
129
+ false
171
130
  end
172
131
 
173
- def new_column_from_field(table_name, field) # :nondoc:
174
- sql_type = field[1]
175
- type_metadata = fetch_type_metadata(sql_type)
176
- ClickhouseColumn.new(field[0], field[3].present? ? field[3] : nil, type_metadata, field[1].include?('Nullable'), table_name, nil)
132
+ def create_schema_dumper(options) # :nodoc:
133
+ ClickhouseActiverecord::SchemaDumper.create(self, options)
177
134
  end
178
135
 
179
- def indexes(_table_name, _name = nil) #:nodoc:
180
- Rails.logger.warn 'Clickhouse indexes is not supported'
181
- []
136
+ # Create a new ClickHouse database.
137
+ def create_database(name)
138
+ sql = "CREATE DATABASE #{quote_table_name(name)}"
139
+ log_with_debug(sql, adapter_name) do
140
+ res = @connection.post("/?#{@config.except(:database).to_param}", "CREATE DATABASE #{quote_table_name(name)}")
141
+ process_response(res)
142
+ end
182
143
  end
183
144
 
184
- def primary_key(table_name) #:nodoc:
185
- pk = table_structure(table_name).first
186
- return 'id' if pk.present? && pk[0] == 'id'
187
- false
145
+ # Drops a ClickHouse database.
146
+ def drop_database(name) #:nodoc:
147
+ sql = "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
148
+ log_with_debug(sql, adapter_name) do
149
+ res = @connection.post("/?#{@config.except(:database).to_param}", sql)
150
+ process_response(res)
151
+ end
188
152
  end
189
153
 
190
- def data_sources
191
- tables
154
+ def drop_table(table_name, options = {}) # :nodoc:
155
+ do_execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}"
192
156
  end
193
157
 
194
158
  protected
195
159
 
196
- def table_structure(table_name)
197
- data = query("DESCRIBE TABLE #{table_name}", table_name)['data']
198
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if data.empty?
199
- data
200
- end
201
- alias column_definitions table_structure
202
-
203
160
  def last_inserted_id(result)
204
161
  result
205
162
  end
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/connection_adapters/clickhouse_adapter'
2
4
 
3
- require 'clickhouse/railtie' if defined?(Rails::Railtie)
5
+ if defined?(Rails::Railtie)
6
+ require 'clickhouse-activerecord/railtie'
7
+ require 'clickhouse-activerecord/schema_dumper'
8
+ require 'clickhouse-activerecord/tasks'
9
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/clickhouse/, "ClickhouseActiverecord::Tasks")
10
+ end
4
11
 
5
12
  module ClickhouseActiverecord
6
13
 
@@ -0,0 +1,21 @@
1
+ require 'arel/visitors/to_sql'
2
+
3
+ module ClickhouseActiverecord
4
+ module Arel
5
+ module Visitors
6
+ class ToSql < ::Arel::Visitors::ToSql
7
+
8
+ def aggregate(name, o, collector)
9
+ # todo how get model class from request? This method works with only rails 4.2.
10
+ # replacing function name for materialized view
11
+ # if o.expressions.first && o.expressions.first != '*' && !o.expressions.first.is_a?(String) && o.expressions.first.relation && o.expressions.first.relation.engine && o.expressions.first.relation.engine.is_view
12
+ # super("#{name.downcase}Merge", o, collector)
13
+ # else
14
+ super
15
+ # end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,4 +1,6 @@
1
- module Clickhouse
1
+ # frozen_string_literal: true
2
+
3
+ module ClickhouseActiverecord
2
4
  require 'rails'
3
5
 
4
6
  class Railtie < Rails::Railtie
@@ -0,0 +1,10 @@
1
+ module ClickhouseActiverecord
2
+ class SchemaDumper < ::ActiveRecord::ConnectionAdapters::SchemaDumper
3
+
4
+ def table(table, stream)
5
+ stream.puts " # TABLE: #{table}"
6
+ stream.puts " # SQL: #{@connection.do_system_execute("SHOW CREATE TABLE #{table.gsub(/^\.inner\./, '')}")['data'].try(:first).try(:first)}"
7
+ super(table.gsub(/^\.inner\./, ''), stream)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClickhouseActiverecord
4
+ class Tasks
5
+
6
+ delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base
7
+
8
+ def initialize(configuration)
9
+ @configuration = configuration
10
+ end
11
+
12
+ def create
13
+ establish_master_connection
14
+ connection.create_database @configuration["database"]
15
+ rescue ActiveRecord::StatementInvalid => e
16
+ if e.cause.to_s.include?('already exists')
17
+ raise ActiveRecord::Tasks::DatabaseAlreadyExists
18
+ else
19
+ raise
20
+ end
21
+ end
22
+
23
+ def drop
24
+ establish_master_connection
25
+ connection.drop_database @configuration["database"]
26
+ end
27
+
28
+ def purge
29
+ clear_active_connections!
30
+ drop
31
+ create
32
+ end
33
+
34
+ def migrate
35
+ check_target_version
36
+
37
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
38
+ scope = ENV["SCOPE"]
39
+ verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, verbose
40
+ binding.pry
41
+ connection.migration_context.migrate(target_version) do |migration|
42
+ scope.blank? || scope == migration.scope
43
+ end
44
+ ActiveRecord::Base.clear_cache!
45
+ ensure
46
+ ActiveRecord::Migration.verbose = verbose_was
47
+ end
48
+
49
+ private
50
+
51
+ def establish_master_connection
52
+ establish_connection @configuration
53
+ end
54
+
55
+ def check_target_version
56
+ if target_version && !(ActiveRecord::Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
57
+ raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
58
+ end
59
+ end
60
+
61
+ def target_version
62
+ ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,3 @@
1
+ module ClickhouseActiverecord
2
+ VERSION = '0.3.1'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/active_record/migration/migration_generator'
2
+
3
+ class ClickhouseMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
4
+ source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates")
5
+
6
+ def create_migration_file
7
+ set_local_assigns!
8
+ validate_file_name!
9
+ migration_template @migration_template, "db/migrate_clickhouse/#{file_name}.rb"
10
+ end
11
+ end
@@ -1,11 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :clickhouse do
2
4
 
5
+ task load_config: :environment do
6
+ ENV['SCHEMA'] = "db/clickhouse_schema.rb"
7
+ ActiveRecord::Migrator.migrations_paths = ["db/migrate_clickhouse"]
8
+ ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
9
+ end
10
+
3
11
  namespace :schema do
4
12
 
5
13
  # todo not testing
6
14
  desc 'Load database schema'
7
- task load: :environment do
8
- ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
15
+ task load: :load_config do
9
16
  load("#{Rails.root}/db/clickhouse_schema.rb")
10
17
  end
11
18
 
@@ -14,10 +21,35 @@ namespace :clickhouse do
14
21
  filename = "#{Rails.root}/db/clickhouse_schema.rb"
15
22
  File.open(filename, 'w:utf-8') do |file|
16
23
  ActiveRecord::Base.establish_connection(:"#{Rails.env}_clickhouse")
17
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
24
+ ClickhouseActiverecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
18
25
  end
19
26
  end
20
27
 
21
28
  end
22
29
 
30
+ desc 'Creates the database from DATABASE_URL or config/database.yml'
31
+ task create: [:load_config] do
32
+ ActiveRecord::Tasks::DatabaseTasks.create(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
33
+ end
34
+
35
+ desc 'Drops the database from DATABASE_URL or config/database.yml'
36
+ task drop: [:load_config, 'db:check_protected_environments'] do
37
+ ActiveRecord::Tasks::DatabaseTasks.drop(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
38
+ end
39
+
40
+ desc 'Empty the database from DATABASE_URL or config/database.yml'
41
+ task purge: [:load_config, 'db:check_protected_environments'] do
42
+ ActiveRecord::Tasks::DatabaseTasks.purge(ActiveRecord::Base.configurations["#{Rails.env}_clickhouse"])
43
+ end
44
+
45
+ # desc 'Resets your database using your migrations for the current environment'
46
+ task reset: :load_config do
47
+ Rake::Task['clickhouse:purge'].execute
48
+ Rake::Task['clickhouse:migrate'].execute
49
+ end
50
+
51
+ desc 'Migrate the clickhouse database'
52
+ task migrate: :load_config do
53
+ Rake::Task['db:migrate'].execute
54
+ end
23
55
  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.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Odintsov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-20 00:00:00.000000000 Z
11
+ date: 2019-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.13.4
27
27
  - !ruby/object:Gem::Dependency
28
- name: rails
28
+ name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '5.0'
33
+ version: '5.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '5.0'
40
+ version: '5.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  description: ActiveRecord adapter for ClickHouse
70
98
  email:
71
99
  - nixx.dj@gmail.com
@@ -74,6 +102,8 @@ extensions: []
74
102
  extra_rdoc_files: []
75
103
  files:
76
104
  - ".gitignore"
105
+ - ".rspec"
106
+ - CHANGELOG.md
77
107
  - CODE_OF_CONDUCT.md
78
108
  - Gemfile
79
109
  - LICENSE.txt
@@ -85,9 +115,17 @@ files:
85
115
  - lib/active_record/connection_adapters/clickhouse/oid/big_integer.rb
86
116
  - lib/active_record/connection_adapters/clickhouse/oid/date.rb
87
117
  - lib/active_record/connection_adapters/clickhouse/oid/date_time.rb
118
+ - lib/active_record/connection_adapters/clickhouse/schema_creation.rb
119
+ - lib/active_record/connection_adapters/clickhouse/schema_definitions.rb
120
+ - lib/active_record/connection_adapters/clickhouse/schema_statements.rb
88
121
  - lib/active_record/connection_adapters/clickhouse_adapter.rb
89
122
  - lib/clickhouse-activerecord.rb
90
- - lib/clickhouse/railtie.rb
123
+ - lib/clickhouse-activerecord/arel/visitors/to_sql.rb
124
+ - lib/clickhouse-activerecord/railtie.rb
125
+ - lib/clickhouse-activerecord/schema_dumper.rb
126
+ - lib/clickhouse-activerecord/tasks.rb
127
+ - lib/clickhouse-activerecord/version.rb
128
+ - lib/generators/clickhouse_migration_generator.rb
91
129
  - lib/tasks/clickhouse.rake
92
130
  homepage: https://github.com/pnixx/clickhouse-activerecord
93
131
  licenses:
@@ -108,10 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
146
  - !ruby/object:Gem::Version
109
147
  version: '0'
110
148
  requirements: []
111
- rubyforge_project:
112
- rubygems_version: 2.6.12
149
+ rubygems_version: 3.0.1
113
150
  signing_key:
114
151
  specification_version: 4
115
152
  summary: ClickHouse ActiveRecord
116
153
  test_files: []
117
- has_rdoc: