kudu_adapter 0.1.0

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