kudu_adapter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +178 -0
  7. data/kudu_adapter.gemspec +33 -0
  8. data/lib/active_record/connection_adapters/kudu/column.rb +17 -0
  9. data/lib/active_record/connection_adapters/kudu/database_statements.rb +41 -0
  10. data/lib/active_record/connection_adapters/kudu/quoting.rb +51 -0
  11. data/lib/active_record/connection_adapters/kudu/schema_creation.rb +89 -0
  12. data/lib/active_record/connection_adapters/kudu/schema_statements.rb +507 -0
  13. data/lib/active_record/connection_adapters/kudu/sql_type_metadata.rb +16 -0
  14. data/lib/active_record/connection_adapters/kudu/table_definition.rb +32 -0
  15. data/lib/active_record/connection_adapters/kudu/type/big_int.rb +22 -0
  16. data/lib/active_record/connection_adapters/kudu/type/boolean.rb +23 -0
  17. data/lib/active_record/connection_adapters/kudu/type/char.rb +17 -0
  18. data/lib/active_record/connection_adapters/kudu/type/date_time.rb +21 -0
  19. data/lib/active_record/connection_adapters/kudu/type/double.rb +17 -0
  20. data/lib/active_record/connection_adapters/kudu/type/float.rb +18 -0
  21. data/lib/active_record/connection_adapters/kudu/type/integer.rb +22 -0
  22. data/lib/active_record/connection_adapters/kudu/type/small_int.rb +22 -0
  23. data/lib/active_record/connection_adapters/kudu/type/string.rb +17 -0
  24. data/lib/active_record/connection_adapters/kudu/type/time.rb +30 -0
  25. data/lib/active_record/connection_adapters/kudu/type/tiny_int.rb +22 -0
  26. data/lib/active_record/connection_adapters/kudu_adapter.rb +173 -0
  27. data/lib/active_record/tasks/kudu_database_tasks.rb +29 -0
  28. data/lib/arel/visitors/kudu.rb +7 -0
  29. data/lib/kudu_adapter/bind_substitution.rb +15 -0
  30. data/lib/kudu_adapter/table_definition_extensions.rb +28 -0
  31. data/lib/kudu_adapter/version.rb +5 -0
  32. data/lib/kudu_adapter.rb +5 -0
  33. data/spec/spec_config.yaml.template +8 -0
  34. data/spec/spec_helper.rb +124 -0
  35. metadata +205 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/type/integer'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Kudu
8
+ module Type
9
+ # :nodoc:
10
+ class Integer < ::ActiveModel::Type::Integer
11
+ def type
12
+ :integer
13
+ end
14
+
15
+ def limit
16
+ 4
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/type/integer'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Kudu
8
+ module Type
9
+ # :nodoc:
10
+ class SmallInt < ::ActiveModel::Type::Integer
11
+ def type
12
+ :smallint
13
+ end
14
+
15
+ def limit
16
+ 2
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/type/string'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Kudu
8
+ module Type
9
+ class String < ::ActiveModel::Type::String
10
+ def type
11
+ :string
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/type/big_integer'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Kudu
8
+ module Type
9
+ # :nodoc:
10
+ class Time < ::ActiveModel::Type::BigInteger
11
+ def type
12
+ :time
13
+ end
14
+
15
+ def serialize(value)
16
+ value.to_i
17
+ end
18
+
19
+ def deserialize(value)
20
+ ::Time.at value.to_i
21
+ end
22
+
23
+ def user_input_in_time_zone(value)
24
+ value.in_time_zone
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/type/integer'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Kudu
8
+ module Type
9
+ # :nodoc:
10
+ class TinyInt < ::ActiveModel::Type::Integer
11
+ def type
12
+ :tinyint
13
+ end
14
+
15
+ def limit
16
+ 1
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/kudu/quoting'
4
+ require 'active_record/connection_adapters/kudu/database_statements'
5
+ require 'active_record/connection_adapters/kudu/schema_statements'
6
+ require 'active_record/connection_adapters/kudu/type/big_int'
7
+ require 'active_record/connection_adapters/kudu/type/boolean'
8
+ require 'active_record/connection_adapters/kudu/type/char'
9
+ require 'active_record/connection_adapters/kudu/type/date_time'
10
+ require 'active_record/connection_adapters/kudu/type/double'
11
+ require 'active_record/connection_adapters/kudu/type/float'
12
+ require 'active_record/connection_adapters/kudu/type/integer'
13
+ require 'active_record/connection_adapters/kudu/type/small_int'
14
+ require 'active_record/connection_adapters/kudu/type/string'
15
+ require 'active_record/connection_adapters/kudu/type/time'
16
+ require 'active_record/connection_adapters/kudu/type/tiny_int'
17
+ require 'impala'
18
+ require 'kudu_adapter/bind_substitution'
19
+
20
+ module ActiveRecord
21
+ # Create new connection with Impala database
22
+ # @param config [::Hash] Connection configuration options
23
+ class Base
24
+ def self.kudu_connection(config)
25
+ ::ActiveRecord::ConnectionAdapters::KuduAdapter.new(nil, logger, config)
26
+ end
27
+ end
28
+
29
+ # :nodoc:
30
+ module Timestamp
31
+ private
32
+ def _create_record
33
+ if record_timestamps
34
+ current_time = current_time_from_proper_timezone
35
+ all_timestamp_attributes_in_model.each do |column|
36
+ # force inserting of current time for timestamp columns if is needed
37
+ write_attribute(column, current_time)
38
+ end
39
+ end
40
+ super
41
+ end
42
+ end
43
+
44
+ # :nodoc:
45
+ module ConnectionAdapters
46
+ # Main Impala connection adapter class
47
+ class KuduAdapter < ::ActiveRecord::ConnectionAdapters::AbstractAdapter
48
+
49
+ include Kudu::DatabaseStatements
50
+ include Kudu::SchemaStatements
51
+ include Kudu::Quoting
52
+
53
+ ADAPTER_NAME = 'Kudu'
54
+
55
+ # @!attribute [r] connection
56
+ # @return [::Impala::Connection] Connection which we are working on
57
+ attr_reader :connection
58
+
59
+ NATIVE_DATABASE_TYPES = {
60
+ primary_key: { name: 'INT' },
61
+ tinyint: { name: 'TINYINT' }, # 1 byte
62
+ smallint: { name: 'SMALLINT' }, # 2 bytes
63
+ integer: { name: 'INT' }, # 4 bytes
64
+ bigint: { name: 'BIGINT' }, # 8 bytes
65
+ float: { name: 'FLOAT' },
66
+ double: { name: 'DOUBLE' },
67
+ boolean: { name: 'BOOLEAN' },
68
+ char: { name: 'CHAR', limit: 255 },
69
+ string: { name: 'STRING' }, # 32767 characters
70
+ time: { name: 'BIGINT' },
71
+ datetime: { name: 'BIGINT' }
72
+ }.freeze
73
+
74
+ def initialize(connection, logger, connection_params)
75
+ super(connection, logger)
76
+
77
+ @connection_params = connection_params
78
+ connect
79
+ @visitor = ::KuduAdapter::BindSubstition.new self
80
+ end
81
+
82
+ def connect
83
+ @connection = ::Impala.connect(
84
+ @connection_params[:host],
85
+ @connection_params[:port]
86
+ )
87
+
88
+ db_names = @connection.query('SHOW DATABASES').map {|db| db[:name]}
89
+
90
+ @connection.execute('USE ' + @connection_params[:database]) if
91
+ @connection_params[:database].present? && db_names.include?(@connection_params[:database])
92
+ end
93
+
94
+ def disconnect!
95
+ @connection.close
96
+ @connection = nil
97
+ end
98
+
99
+ def reconnect!
100
+ disconnect!
101
+ connect
102
+ end
103
+
104
+ def active?
105
+ @connection.execute('SELECT now()')
106
+ true
107
+ rescue
108
+ false
109
+ end
110
+
111
+ def execute(sql, name = nil)
112
+ with_auto_reconnect do
113
+ log(sql, name) { @connection.execute(sql) }
114
+ end
115
+ end
116
+
117
+ def query(sql, name = nil)
118
+ with_auto_reconnect do
119
+ log(sql, name) do
120
+ @connection.query sql
121
+ end
122
+ end
123
+ end
124
+
125
+ def lookup_cast_type_from_column(column)
126
+ lookup_cast_type column.type.to_s
127
+ end
128
+
129
+ def supports_migrations?
130
+ true
131
+ end
132
+
133
+ def supports_multi_insert?
134
+ false
135
+ end
136
+
137
+ def supports_primary_key?
138
+ true
139
+ end
140
+
141
+ def native_database_types
142
+ ::ActiveRecord::ConnectionAdapters::KuduAdapter::NATIVE_DATABASE_TYPES
143
+ end
144
+
145
+ def initialize_type_map(mapping)
146
+ mapping.register_type(/bigint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::BigInt.new)
147
+ mapping.register_type(/boolean/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Boolean.new)
148
+ mapping.register_type(/char/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Char.new)
149
+ mapping.register_type(/datetime/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::DateTime.new)
150
+ mapping.register_type(/double/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Double.new)
151
+ mapping.register_type(/float/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Float.new)
152
+ mapping.register_type(/integer/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Integer.new)
153
+ mapping.register_type(/smallint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::SmallInt.new)
154
+ mapping.register_type(/string/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::String.new)
155
+ mapping.register_type(/(date){0}time/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Time.new)
156
+ mapping.register_type(/tinyint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::TinyInt.new)
157
+ end
158
+
159
+ def with_auto_reconnect
160
+ yield
161
+ rescue Thrift::TransportException => e
162
+ raise unless e.message == 'end of file reached'
163
+ reconnect!
164
+ yield
165
+ end
166
+
167
+ def create_database(database_name)
168
+ # TODO: escape name
169
+ execute "CREATE DATABASE IF NOT EXISTS `#{database_name}`"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/tasks/database_tasks'
4
+
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ module Tasks
8
+ # :nodoc:
9
+ class KuduDatabaseTasks
10
+ delegate :connection, :establish_connection, :clear_active_connections!,
11
+ to: ::ActiveRecord::Base
12
+
13
+ # @!attribute [r] configuration
14
+ # @return [Hash] Database configuration
15
+ attr_reader :configuration
16
+
17
+ def initialize(configuration)
18
+ @configuration = configuration
19
+ end
20
+
21
+ def create
22
+ establish_connection
23
+ connection.create_database configuration['database']
24
+ end
25
+ end
26
+
27
+ DatabaseTasks.register_task(/kudu/, KuduDatabaseTasks)
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel
4
+ module Visitors
5
+ class Kudu < ::Arel::Visitors::ToSql; end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arel/visitors/bind_visitor'
4
+ require 'arel/visitors/kudu'
5
+
6
+ module KuduAdapter
7
+ # Bind substition class definition
8
+ class BindSubstition < ::Arel::Visitors::Kudu
9
+ include ::Arel::Visitors::BindVisitor
10
+
11
+ def preparable
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KuduAdapter
4
+ # Definitions of additional table capabilities in Kudu
5
+ module TableDefinitionExtensions
6
+ # @!attribute [r] partitions
7
+ # @return [Array] List of table partition definitions
8
+ attr_reader :partitions
9
+
10
+ # Define single partition
11
+ # @param name [String] Parition name
12
+ # @param type [String] Partition type
13
+ # @param options [Hash] Parition options
14
+ def partition(name, type, options = {})
15
+ column(name, type, options)
16
+ @partitions ||= []
17
+ @partitions << @columns.pop
18
+ end
19
+
20
+ def row_format
21
+ 'ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"'
22
+ end
23
+
24
+ def external
25
+ true
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KuduAdapter
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requiring with this pattern to mirror ActiveRecord
4
+ require 'active_record/connection_adapters/kudu_adapter'
5
+ require 'active_record/tasks/kudu_database_tasks'
@@ -0,0 +1,8 @@
1
+ # Copy this file to spec/config.yaml and set appropriate values.
2
+ # You can also use environment variables, see spec_helper.rb
3
+ database:
4
+ name: 'test'
5
+ host: '127.0.0.1'
6
+ port: 28050
7
+ user: 'kudu'
8
+ password: 'impala'
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'rubygems'
7
+ require 'bundler'
8
+ require 'yaml'
9
+
10
+ Bundler.setup :default, :development
11
+
12
+ $LOAD_PATH.unshift(File.expand('../../lib', __FILE__))
13
+
14
+ # Load config, if present
15
+ config_path = File.expand('../spec_config.yaml', __FILE__)
16
+ config = if File.exist?(config_path)
17
+ puts "==> Loading config from #{config_path}"
18
+ YAML.load_file config_path
19
+ else
20
+ puts '==> Loading config from env or use default'
21
+ {
22
+ 'database' => {}
23
+ }
24
+ end
25
+
26
+ require 'rspec'
27
+
28
+ require 'active_record'
29
+
30
+ require 'active_support/core_ext/module/attribute_accessors'
31
+ require 'active_support/core_ext/class/attribute_accessors'
32
+
33
+ require 'active_support/log_subscriber'
34
+ require 'active_record/log_subscriber'
35
+
36
+ require 'logger'
37
+
38
+ require 'active_record/connection_adapters/kudu_adapter'
39
+
40
+ puts "==> Effective ActiveRecord version #{ActiveRecord::VERSION::STRING}"
41
+
42
+ # :nodoc:
43
+ module LoggerSpecHelper
44
+ def set_logger
45
+ @logger = MockLogger.new
46
+ @old_logger = ActiveRecord::Base.logger
47
+
48
+ @notifier = ActiveSupport::Notifications::Fanout.new
49
+
50
+ ActiveSupport::LogSubscriber.colorize_logging = false
51
+
52
+ ActiveRecord::Base.logger = @logger
53
+
54
+ @old_notifier = ActiveSupport::Notifications.notifier
55
+ ActiveSupport::Notifications.notifier = @notifier
56
+
57
+ ActiveRecord::LogSubscriber.attach_to :active_record
58
+ ActiveSupport::Notifications.subscribe 'sql.active_record',
59
+ ActiveRecord::ExplainSubscriber.new
60
+ end
61
+
62
+ # :nodoc:
63
+ class MockLogger
64
+ attr_reader :flush_count
65
+
66
+ def initialize
67
+ @flush_count = 0
68
+ @logged = Hash.new { |h, k| h[k] = [] }
69
+ end
70
+
71
+ def method_missing(level, message)
72
+ if respond_to_missing?(level)
73
+ @logged[level] << message
74
+ else
75
+ super
76
+ end
77
+ end
78
+
79
+ def respond_to_missing?(method, *)
80
+ %i[debug info warn error].include?(method) || super
81
+ end
82
+
83
+ def logged(level)
84
+ @logged[level].compact.map { |l| l.to_s.strip }
85
+ end
86
+
87
+ def output(level)
88
+ logged(level).join "\n"
89
+ end
90
+
91
+ def flush
92
+ @flush_count += 1
93
+ end
94
+
95
+ def clear(level)
96
+ @logged[level] = []
97
+ end
98
+ end
99
+ end
100
+
101
+ DATABASE_NAME = config['database']['name'] ||
102
+ ENV['DATABASE_NAME'] ||
103
+ 'test'
104
+ DATABASE_HOST = config['database']['host'] ||
105
+ ENV['DATABASE_HOST'] ||
106
+ '127.0.0.1'
107
+ DATABASE_PORT = config['database']['port'] ||
108
+ ENV['DATABASE_PORT'] ||
109
+ 28_050
110
+ DATABASE_USER = config['database']['user'] ||
111
+ ENV['DATABASE_USER'] ||
112
+ 'kudu'
113
+ DATABASE_PASSWORD = config['database']['password'] ||
114
+ ENV['DATABASE_PASSWORD'] ||
115
+ 'impala'
116
+
117
+ CONNECTION_PARAMS = {
118
+ adapter: 'kudu',
119
+ database: DATABASE_NAME,
120
+ host: DATABASE_HOST,
121
+ port: DATABASE_PORT,
122
+ username: DATABASE_USER,
123
+ password: DATABASE_PASSWORD
124
+ }.freeze