clickhouse-activerecord 0.2.2 → 0.3.1

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